@accord-dev/accord 0.2.0 → 0.3.0

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/README.md +153 -26
  2. package/dist/cli.js +945 -309
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -20818,6 +20818,38 @@ var config_schema_default = {
20818
20818
  "type": "string",
20819
20819
  "enum": ["allow", "forbid"]
20820
20820
  } }
20821
+ },
20822
+ "generated": {
20823
+ "type": "array",
20824
+ "uniqueItems": true,
20825
+ "items": {
20826
+ "type": "string",
20827
+ "minLength": 1,
20828
+ "allOf": [
20829
+ { "pattern": "^[A-Za-z0-9._@-](?:[A-Za-z0-9._@/ -]*[A-Za-z0-9._@/-])?$" },
20830
+ { "pattern": "^(?!(?:.*/)?\\.\\.(?:/|$))" },
20831
+ { "pattern": "^(?!(?:.*/)?\\.(?:/|$))" },
20832
+ { "pattern": "^(?!.*//)" }
20833
+ ]
20834
+ }
20835
+ },
20836
+ "sensitive": {
20837
+ "type": "object",
20838
+ "propertyNames": { "pattern": "^[a-z][a-z0-9-]*$" },
20839
+ "additionalProperties": {
20840
+ "type": "array",
20841
+ "uniqueItems": true,
20842
+ "items": {
20843
+ "type": "string",
20844
+ "minLength": 1,
20845
+ "allOf": [
20846
+ { "pattern": "^[A-Za-z0-9._@-](?:[A-Za-z0-9._@/ -]*[A-Za-z0-9._@/-])?$" },
20847
+ { "pattern": "^(?!(?:.*/)?\\.\\.(?:/|$))" },
20848
+ { "pattern": "^(?!(?:.*/)?\\.(?:/|$))" },
20849
+ { "pattern": "^(?!.*//)" }
20850
+ ]
20851
+ }
20852
+ }
20821
20853
  }
20822
20854
  }
20823
20855
  };
@@ -20994,6 +21026,29 @@ function loadConfig(file, text) {
20994
21026
  findings
20995
21027
  };
20996
21028
  }
21029
+ /**
21030
+ * ST-LN-62: the `generated:` and `sensitive:` lists of a config text, read without validating it, so a config
21031
+ * that fails the schema still gives the lists that parse. Never throws; no text, or text that is not a mapping, gives two empty lists.
21032
+ */
21033
+ function configLists(text) {
21034
+ const none = {
21035
+ generated: [],
21036
+ sensitive: []
21037
+ };
21038
+ if (text === void 0) return none;
21039
+ const { map } = parseYamlMap("accord/config.yml", normaliseText(text), 0);
21040
+ if (map === void 0) return none;
21041
+ const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
21042
+ const s = map.sensitive;
21043
+ const sensitive = typeof s === "object" && s !== null && !Array.isArray(s) ? Object.keys(s).flatMap((area) => strings(s[area]).map((path) => ({
21044
+ area,
21045
+ path
21046
+ }))) : [];
21047
+ return {
21048
+ generated: strings(map.generated),
21049
+ sensitive
21050
+ };
21051
+ }
20997
21052
  const LANGUAGE = /^#\s*language:\s*([A-Za-z-]+)/;
20998
21053
  const AC_TAG = /^@ac-[1-9][0-9]*$/;
20999
21054
  const escape$1 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -21430,8 +21485,16 @@ const TASK$1 = /^Task\s+([1-9][0-9]*)\s*:\s*(.*)$/;
21430
21485
  const FIELD = /^(covers|files|verify):(.*)$/;
21431
21486
  const DONE$1 = /^\s*[-*+]\s+task\s+([1-9][0-9]*)\s+done\s+([0-9a-fA-F]{7,})\s*$/;
21432
21487
  const RULING = /^\s*(?:[-*+]\s+)?(Ruling:.*\S)\s*$/;
21488
+ const WORDING = /^\s*(?:[-*+]\s+)?Wording-only:\s+(\S.*?)\s+—\s+(.*\S)\s*$/;
21489
+ const CONFIRMED = /^Confirmed wording-only:\s+(.+)$/;
21490
+ const NOT_INTERFACE = /^Not an interface:\s+(\S.*?)\s+—\s+(.*\S)$/;
21433
21491
  const VERDICT = /^verdict:\s*(\S+)(?:\s.*)?$/;
21434
21492
  const MARKER$1 = /^\(@ac-[1-9][0-9]*\)$/;
21493
+ /** A declared path, trimmed, with one pair of surrounding backticks removed. */
21494
+ const declared = (t) => {
21495
+ const x = t.trim();
21496
+ return x.length >= 2 && x.startsWith("`") && x.endsWith("`") ? x.slice(1, -1).trim() : x;
21497
+ };
21435
21498
  /** A field value: tokens separated by whitespace or commas, empty tokens dropped. */
21436
21499
  const tokens$1 = (v) => v.split(/[\s,]+/).filter((x) => x !== "");
21437
21500
  /** The parser's own field rule: first `covers:`/`files:`/`verify:` line per field, outside a fence, list marker stripped. */
@@ -21533,7 +21596,13 @@ function parsePlan(file, text) {
21533
21596
  function parseLedger(file, text) {
21534
21597
  const done = {};
21535
21598
  const rulings = [];
21599
+ const wordingOnly = [];
21536
21600
  for (const line of normaliseText(text).split("\n")) {
21601
+ const w = WORDING.exec(line);
21602
+ if (w) wordingOnly.push({
21603
+ path: declared(w[1]),
21604
+ reason: w[2]
21605
+ });
21537
21606
  const d = DONE$1.exec(line);
21538
21607
  if (d) {
21539
21608
  if (!Object.hasOwn(done, d[1])) done[d[1]] = d[2];
@@ -21543,22 +21612,26 @@ function parseLedger(file, text) {
21543
21612
  if (r) rulings.push(r[1]);
21544
21613
  }
21545
21614
  const section = scan(normaliseText(text), 0).sections.find((s) => headingKey(s.heading) === "rulings");
21615
+ const extra = wordingOnly.length > 0 ? { wordingOnly } : {};
21546
21616
  if (section === void 0) return {
21547
21617
  file,
21548
21618
  done,
21549
- rulings
21619
+ rulings,
21620
+ ...extra
21550
21621
  };
21551
21622
  return {
21552
21623
  file,
21553
21624
  done,
21554
21625
  rulings,
21555
- rulingsSection: stripHtmlComments(section.lines).map((l) => l.text).filter((t) => t.trim() !== "")
21626
+ rulingsSection: stripHtmlComments(section.lines).map((l) => l.text).filter((t) => t.trim() !== ""),
21627
+ ...extra
21556
21628
  };
21557
21629
  }
21558
- /** The last `verdict:` line decides; a record with none has no verdict. */
21630
+ /** The last `verdict:` line decides; a record with none has no verdict. The last `## Round` also yields its declaration lines. */
21559
21631
  function parseReview(file, text) {
21560
21632
  let review = { file };
21561
- normaliseText(text).split("\n").forEach((line, i) => {
21633
+ const normal = normaliseText(text);
21634
+ normal.split("\n").forEach((line, i) => {
21562
21635
  const m = VERDICT.exec(line.trim());
21563
21636
  if (m) review = {
21564
21637
  file,
@@ -21570,6 +21643,24 @@ function parseReview(file, text) {
21570
21643
  trailing: true
21571
21644
  };
21572
21645
  });
21646
+ const rounds = scan(normal, 0).sections.filter((s) => headingKey(s.heading).startsWith("round "));
21647
+ const confirmed = [];
21648
+ const notInterface = [];
21649
+ for (const l of rounds[rounds.length - 1]?.lines ?? []) {
21650
+ const t = l.text.replace(LIST_MARKER, "").trim();
21651
+ const c = CONFIRMED.exec(t);
21652
+ if (c) confirmed.push(declared(c[1]));
21653
+ const n = NOT_INTERFACE.exec(t);
21654
+ if (n) notInterface.push(declared(n[1]));
21655
+ }
21656
+ if (confirmed.length > 0) review = {
21657
+ ...review,
21658
+ confirmed
21659
+ };
21660
+ if (notInterface.length > 0) review = {
21661
+ ...review,
21662
+ notInterface
21663
+ };
21573
21664
  return review;
21574
21665
  }
21575
21666
  /** One report case id per line; blank lines and comments (`#` alone or `# ` plus text) are skipped; `#123-x` is an id; sorted and unique. */
@@ -21734,7 +21825,8 @@ function loadSnapshot(input) {
21734
21825
  ...input.claimTemplate === void 0 ? {} : { claimTemplate: input.claimTemplate },
21735
21826
  ...input.planFacts === void 0 ? {} : { planFacts: input.planFacts },
21736
21827
  ...input.afterVerify === void 0 ? {} : { afterVerify: input.afterVerify },
21737
- ...input.reviewFacts === void 0 ? {} : { reviewFacts: input.reviewFacts }
21828
+ ...input.reviewFacts === void 0 ? {} : { reviewFacts: input.reviewFacts },
21829
+ ...input.branchFacts === void 0 ? {} : { branchFacts: input.branchFacts }
21738
21830
  };
21739
21831
  }
21740
21832
  /** The ids `id` depends on that exist in the snapshot. */
@@ -22750,8 +22842,8 @@ const RULES = [
22750
22842
  check: prototypeDerivation
22751
22843
  }
22752
22844
  ];
22753
- const cmp$6 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
22754
- const byFileLineRule$1 = (a, b) => cmp$6(a.file, b.file) || (a.line ?? 0) - (b.line ?? 0) || cmp$6(a.rule, b.rule) || cmp$6(a.reason, b.reason) || cmp$6(a.pointer ?? "", b.pointer ?? "");
22845
+ const cmp$7 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
22846
+ const byFileLineRule$1 = (a, b) => cmp$7(a.file, b.file) || (a.line ?? 0) - (b.line ?? 0) || cmp$7(a.rule, b.rule) || cmp$7(a.reason, b.reason) || cmp$7(a.pointer ?? "", b.pointer ?? "");
22755
22847
  /** Pure: reads the snapshot only, never mutates it, and returns the same result on every call. */
22756
22848
  function lintSnapshot(snapshot) {
22757
22849
  const profile = snapshot.config?.profile ?? "build";
@@ -22776,7 +22868,7 @@ function forbiddenLines(message) {
22776
22868
  return message.split("\n").slice(1).filter((l) => /^\s*[A-Za-z0-9-]*-by:/i.test(l) || /generated with/i.test(l)).map((l) => l.trim());
22777
22869
  }
22778
22870
  const trailerReason = (sha, line) => `commit ${sha.slice(0, 12)} carries the forbidden line "${line}"; commits.trailers is forbid in accord/config.yml`;
22779
- const ticketFile$3 = (id) => "accord/tickets/" + id + ".md";
22871
+ const ticketFile$4 = (id) => "accord/tickets/" + id + ".md";
22780
22872
  const verificationFile = (id) => "accord/tickets/" + id + "/verification.md";
22781
22873
  const ticketOf$2 = (snapshot, id) => Object.hasOwn(snapshot.tickets, id) ? snapshot.tickets[id] : void 0;
22782
22874
  const verificationOf$1 = (snapshot, id) => Object.hasOwn(snapshot.verifications, id) ? snapshot.verifications[id] : void 0;
@@ -22811,7 +22903,7 @@ function shaEqual(a, b) {
22811
22903
  const noScenarios = (snapshot, id) => {
22812
22904
  if (ticketOf$2(snapshot, id) === void 0 || scenarioTags(snapshot, id).length > 0) return [];
22813
22905
  return [{
22814
- file: ticketFile$3(id),
22906
+ file: ticketFile$4(id),
22815
22907
  ...at(acLine(snapshot, id)),
22816
22908
  reason: "no scenario carries an @ac-n tag, so Done has nothing to be claimed about"
22817
22909
  }];
@@ -22838,7 +22930,7 @@ const tagsDiffer = (snapshot, id) => {
22838
22930
  ].every((s) => s.length === union.length)) return [];
22839
22931
  const lacks = (set) => fmt(union.filter((t) => !set.includes(t)));
22840
22932
  return [{
22841
- file: ticketFile$3(id),
22933
+ file: ticketFile$4(id),
22842
22934
  ...at(acLine(snapshot, id)),
22843
22935
  reason: `scenarios lack ${lacks(scenarios)}; evidence lacks ${lacks(evidence)}; verified lacks ${lacks(verified)}`
22844
22936
  }];
@@ -22872,7 +22964,7 @@ const computed = (snapshot, id) => acHash(ticketOf$2(snapshot, id)?.scenarios ??
22872
22964
  const commitMissing = (snapshot, id) => {
22873
22965
  if (ticketOf$2(snapshot, id) === void 0 || snapshot.git?.commit !== void 0) return [];
22874
22966
  return [{
22875
- file: ticketFile$3(id),
22967
+ file: ticketFile$4(id),
22876
22968
  reason: "the host supplied no commit for the gated tree; Done cannot bind a tick"
22877
22969
  }];
22878
22970
  };
@@ -22888,11 +22980,11 @@ const shaTooShort = (snapshot, id) => {
22888
22980
  const bad = (v) => `"${v}" is not a commit sha; at least 7 hexadecimal characters are needed`;
22889
22981
  return [
22890
22982
  ...host !== void 0 && !isSha(host) ? [{
22891
- file: ticketFile$3(id),
22983
+ file: ticketFile$4(id),
22892
22984
  reason: "host commit " + bad(host)
22893
22985
  }] : [],
22894
22986
  ...tick !== void 0 && !isSha(tick) ? [{
22895
- file: ticketFile$3(id),
22987
+ file: ticketFile$4(id),
22896
22988
  pointer: "/verified_commit",
22897
22989
  reason: "verified_commit " + bad(tick)
22898
22990
  }] : [],
@@ -22908,7 +23000,7 @@ const acHashMissing = (snapshot, id) => {
22908
23000
  const fm = frontmatterOf(snapshot, id);
22909
23001
  if (fm === void 0 || fm.ac_hash !== void 0 || computed(snapshot, id) === void 0) return [];
22910
23002
  return [{
22911
- file: ticketFile$3(id),
23003
+ file: ticketFile$4(id),
22912
23004
  pointer: "/ac_hash",
22913
23005
  reason: "Ready has recorded no ac_hash for this ticket"
22914
23006
  }];
@@ -22922,7 +23014,7 @@ const acChanged = (snapshot, id) => {
22922
23014
  const now = computed(snapshot, id);
22923
23015
  if (recorded === void 0 || now === void 0 || recorded === now) return [];
22924
23016
  return [{
22925
- file: ticketFile$3(id),
23017
+ file: ticketFile$4(id),
22926
23018
  pointer: "/ac_hash",
22927
23019
  reason: `the acceptance criteria changed since Ready: ac_hash records ${recorded}, they hash to ${now} now`
22928
23020
  }];
@@ -22934,7 +23026,7 @@ const tickUnbound = (snapshot, id) => {
22934
23026
  const missing = [...fm.verified_hash === void 0 ? ["verified_hash"] : [], ...fm.verified_commit === void 0 ? ["verified_commit"] : []];
22935
23027
  if (missing.length === 0) return [];
22936
23028
  return [{
22937
- file: ticketFile$3(id),
23029
+ file: ticketFile$4(id),
22938
23030
  pointer: "/verified",
22939
23031
  reason: `verified is set but ${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} not recorded`
22940
23032
  }];
@@ -22945,13 +23037,13 @@ const tickStaleHash = (snapshot, id) => {
22945
23037
  const now = computed(snapshot, id);
22946
23038
  if (recorded === void 0 || now === void 0 || recorded === now) return [];
22947
23039
  return [{
22948
- file: ticketFile$3(id),
23040
+ file: ticketFile$4(id),
22949
23041
  pointer: "/verified_hash",
22950
23042
  reason: `the ticks were made against ${recorded}; the acceptance criteria hash to ${now} now`
22951
23043
  }];
22952
23044
  };
22953
23045
  /** Every path is the ticket's own file, lies in its folder (spec §11), or is accord/product/conventions.md (ST-LN-8 D-08). */
22954
- const ownRecordsOnly = (id, paths) => paths.every((p) => p === ticketFile$3(id) || p === "accord/product/conventions.md" || p.startsWith("accord/tickets/" + id + "/"));
23046
+ const ownRecordsOnly = (id, paths) => paths.every((p) => p === ticketFile$4(id) || p === "accord/product/conventions.md" || p.startsWith("accord/tickets/" + id + "/"));
22955
23047
  /** A commit that changes a path outside the ticket's own records, or a merge whose own changes git could not show. */
22956
23048
  const handMade = (id, c) => c.files === void 0 || !ownRecordsOnly(id, c.files);
22957
23049
  /** A later commit that changed only this ticket's own records (its file and folder) or accord/product/conventions.md is the same reviewed code. */
@@ -22968,7 +23060,7 @@ const tickStaleCommit = (snapshot, id) => {
22968
23060
  if (tick === void 0 || host === void 0 || !isSha(tick) || !isSha(host) || shaEqual(tick, host)) return [];
22969
23061
  if (onlyOwnRecords(snapshot, id, tick)) return [];
22970
23062
  return [{
22971
- file: ticketFile$3(id),
23063
+ file: ticketFile$4(id),
22972
23064
  pointer: "/verified_commit",
22973
23065
  reason: `the ticks were made against commit ${tick}; the gated commit is ${host}`
22974
23066
  }];
@@ -23018,7 +23110,7 @@ const reportLoaded = (snapshot) => {
23018
23110
  * suppress the per-scenario join, never this.
23019
23111
  */
23020
23112
  const testTagMissing = (snapshot, id) => machineScenarios(snapshot, id).filter((s) => !s.tags.some(isTest)).map((s) => ({
23021
- file: ticketFile$3(id),
23113
+ file: ticketFile$4(id),
23022
23114
  line: s.line,
23023
23115
  reason: `scenario "${s.name}" carries no @test:<id> tag and is not @ui, so Done cannot check it against the report`
23024
23116
  }));
@@ -23058,7 +23150,7 @@ const testUnknown = (snapshot, id) => {
23058
23150
  if (!reportLoaded(snapshot)) return [];
23059
23151
  const report = reportPath(snapshot);
23060
23152
  return testIds(snapshot, id).filter(({ testId }) => !Object.hasOwn(snapshot.tests ?? {}, testId)).map(({ line, testId }) => ({
23061
- file: ticketFile$3(id),
23153
+ file: ticketFile$4(id),
23062
23154
  line,
23063
23155
  reason: `@test:${testId} not found in ${report}; ids are classname#name with spaces as '-'`
23064
23156
  }));
@@ -23072,7 +23164,7 @@ const testNotPassed = (snapshot, id) => {
23072
23164
  const tests = snapshot.tests ?? {};
23073
23165
  const report = reportPath(snapshot);
23074
23166
  return testIds(snapshot, id).filter(({ testId }) => Object.hasOwn(tests, testId) && tests[testId] !== "passed").map(({ line, testId }) => ({
23075
- file: ticketFile$3(id),
23167
+ file: ticketFile$4(id),
23076
23168
  line,
23077
23169
  reason: `@test:${testId} is recorded ${tests[testId]} in ${report}; Done passes on passed alone`
23078
23170
  }));
@@ -23405,14 +23497,14 @@ const testsBaselineMissing = (snapshot, id) => {
23405
23497
  reason: "accord/tests-baseline.txt does not exist, so every test case already in the suite counts as untraced at the Done gate; ask the owner, at the plan stage's baseline tests step and never before it, whether to run accord tests baseline"
23406
23498
  }];
23407
23499
  };
23408
- const ticketFile$2 = (id) => "accord/tickets/" + id + ".md";
23500
+ const ticketFile$3 = (id) => "accord/tickets/" + id + ".md";
23409
23501
  const ticketOf$1 = (snapshot, id) => Object.hasOwn(snapshot.tickets, id) ? snapshot.tickets[id] : void 0;
23410
23502
  const section$1 = (snapshot, id, key) => ticketOf$1(snapshot, id)?.sections.find((s) => headingKey(s.heading) === key);
23411
23503
  /** Spec §3: the key that opts a ticket into the workflow-layer contract. */
23412
23504
  const laneOf = (snapshot, id) => ticketOf$1(snapshot, id)?.frontmatter?.lane;
23413
23505
  /** The gated id is not in the snapshot; this is the whole Ready result. */
23414
23506
  const ticketUnknown = (snapshot, id) => ticketOf$1(snapshot, id) === void 0 ? [{
23415
- file: ticketFile$2(id),
23507
+ file: ticketFile$3(id),
23416
23508
  reason: "no ticket " + id + " in the snapshot"
23417
23509
  }] : [];
23418
23510
  /**
@@ -23425,7 +23517,7 @@ const designMissing = (snapshot, id) => {
23425
23517
  const proto = "accord/assets/" + id + "/prototype.html";
23426
23518
  const hasProto = Object.hasOwn(snapshot.files, proto);
23427
23519
  const hasLink = typeof frontmatter.design === "string" && frontmatter.design !== "";
23428
- const file = ticketFile$2(id);
23520
+ const file = ticketFile$3(id);
23429
23521
  if ((snapshot.config?.profile ?? "build") === "maintain") {
23430
23522
  if (hasProto) return [];
23431
23523
  return [{
@@ -23450,7 +23542,7 @@ const intentEmpty = (snapshot, id) => {
23450
23542
  if (s === void 0) return [];
23451
23543
  if (stripHtmlComments(s.lines).some((l) => l.text.trim() !== "")) return [];
23452
23544
  return [{
23453
- file: ticketFile$2(id),
23545
+ file: ticketFile$3(id),
23454
23546
  line: s.line,
23455
23547
  reason: "\"## Intent\" is empty; Ready needs the intent written"
23456
23548
  }];
@@ -23467,7 +23559,7 @@ const earsMissing = (snapshot, id) => {
23467
23559
  if (s === void 0 || ticket === void 0) return [];
23468
23560
  if (ticket.requirements.some((l) => "pattern" in classifyEars(l.text))) return [];
23469
23561
  return [{
23470
- file: ticketFile$2(id),
23562
+ file: ticketFile$3(id),
23471
23563
  line: s.line,
23472
23564
  reason: "\"## Requirements\" has no EARS line; at least one must name \"the system shall\""
23473
23565
  }];
@@ -23479,7 +23571,7 @@ const approachEmpty = (snapshot, id) => {
23479
23571
  if (s === void 0) return [];
23480
23572
  if (stripHtmlComments(s.lines).some((l) => l.text.trim() !== "")) return [];
23481
23573
  return [{
23482
- file: ticketFile$2(id),
23574
+ file: ticketFile$3(id),
23483
23575
  line: s.line,
23484
23576
  reason: "\"## Approach\" is empty; the owner approves an approach"
23485
23577
  }];
@@ -23491,7 +23583,7 @@ const sectionEmpty = (snapshot, id) => {
23491
23583
  const s = section$1(snapshot, id, headingKey(name));
23492
23584
  if (s === void 0 || stripHtmlComments(s.lines).some((l) => l.text.trim() !== "")) return [];
23493
23585
  return [{
23494
- file: ticketFile$2(id),
23586
+ file: ticketFile$3(id),
23495
23587
  line: s.line,
23496
23588
  reason: `"## ${name}" is empty; a lane S ticket needs it written before any change`
23497
23589
  }];
@@ -23502,7 +23594,7 @@ const approvalMissing = (snapshot, id) => {
23502
23594
  const fm = ticketOf$1(snapshot, id)?.frontmatter;
23503
23595
  if (fm?.lane === void 0 || fm.lane === "S" || fm.approved_hash !== void 0) return [];
23504
23596
  return [{
23505
- file: ticketFile$2(id),
23597
+ file: ticketFile$3(id),
23506
23598
  pointer: "/approved_hash",
23507
23599
  reason: "the owner has not approved this ticket; run accord approve " + id + " after the owner says yes"
23508
23600
  }];
@@ -23515,12 +23607,12 @@ const approvalStale = (snapshot, id) => {
23515
23607
  const now = approvedHash(ticket);
23516
23608
  if (recorded === now) return [];
23517
23609
  return [{
23518
- file: ticketFile$2(id),
23610
+ file: ticketFile$3(id),
23519
23611
  pointer: "/approved_hash",
23520
23612
  reason: `the approved sections changed since approval: approved_hash records ${recorded}, they hash to ${now} now; the owner must approve again`
23521
23613
  }];
23522
23614
  };
23523
- const ticketFile$1 = (id) => "accord/tickets/" + id + ".md";
23615
+ const ticketFile$2 = (id) => "accord/tickets/" + id + ".md";
23524
23616
  const laneS = (snapshot, id) => {
23525
23617
  const t = Object.hasOwn(snapshot.tickets, id) ? snapshot.tickets[id] : void 0;
23526
23618
  return t?.frontmatter?.lane === "S" ? t : void 0;
@@ -23529,7 +23621,7 @@ const readyMissing = (snapshot, id) => {
23529
23621
  const t = laneS(snapshot, id);
23530
23622
  if (t === void 0 || t.frontmatter?.ac_hash !== void 0) return [];
23531
23623
  return [{
23532
- file: ticketFile$1(id),
23624
+ file: ticketFile$2(id),
23533
23625
  pointer: "/ac_hash",
23534
23626
  reason: `gate ready has recorded no ac_hash for this lane S ticket; run accord gate ready ${id}`
23535
23627
  }];
@@ -23541,7 +23633,7 @@ const sectionsChanged = (snapshot, id) => {
23541
23633
  const now = laneSHash(t);
23542
23634
  if (recorded === now) return [];
23543
23635
  return [{
23544
- file: ticketFile$1(id),
23636
+ file: ticketFile$2(id),
23545
23637
  pointer: "/ac_hash",
23546
23638
  reason: `## Understood or ## Done when changed since gate ready: ac_hash records ${recorded}, they hash to ${now} now; run accord gate ready ${id} and review again`
23547
23639
  }];
@@ -23555,11 +23647,11 @@ const reviewTrailing = (snapshot, id) => {
23555
23647
  reason: "a line follows the verdict in review.md; the verdict is its last line"
23556
23648
  }];
23557
23649
  };
23558
- const factOf = (snapshot, id) => snapshot.reviewFacts !== void 0 && Object.hasOwn(snapshot.reviewFacts, id) ? snapshot.reviewFacts[id] : void 0;
23650
+ const factOf$1 = (snapshot, id) => snapshot.reviewFacts !== void 0 && Object.hasOwn(snapshot.reviewFacts, id) ? snapshot.reviewFacts[id] : void 0;
23559
23651
  const reviewUncommitted = (snapshot, id) => {
23560
23652
  const review = reviewOf(snapshot, id);
23561
23653
  if (review === void 0) return [];
23562
- const f = factOf(snapshot, id);
23654
+ const f = factOf$1(snapshot, id);
23563
23655
  if (f === void 0) return [{
23564
23656
  file: review.file,
23565
23657
  reason: "the host loaded no commit facts for review.md, so its commit cannot be checked"
@@ -23572,7 +23664,7 @@ const reviewUncommitted = (snapshot, id) => {
23572
23664
  };
23573
23665
  const reviewCommitCode = (snapshot, id) => {
23574
23666
  const review = reviewOf(snapshot, id);
23575
- const files = factOf(snapshot, id)?.files;
23667
+ const files = factOf$1(snapshot, id)?.files;
23576
23668
  if (review === void 0 || files === void 0 || ownRecordsOnly(id, files)) return [];
23577
23669
  const out = files.filter((p) => !ownRecordsOnly(id, [p])).slice(0, 5);
23578
23670
  return [{
@@ -23593,6 +23685,177 @@ const commitAfterReview = (snapshot, id) => {
23593
23685
  reason: `commit ${c.sha} after the review changes ${c.files === void 0 ? "what git could not show" : c.files.filter((p) => !ownRecordsOnly(id, [p])).join(" ")}; a change after review needs another round and a new review.md`
23594
23686
  }));
23595
23687
  };
23688
+ const ticketFile$1 = (id) => "accord/tickets/" + id + ".md";
23689
+ const cmp$6 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
23690
+ /** The paths a ticket's `## Scope` section lists, one per line, or undefined when it has no such section. */
23691
+ function scopeLines(text) {
23692
+ const { body, bodyOffset } = splitFrontmatter(normaliseText(text));
23693
+ const section = scan(body, bodyOffset).sections.find((s) => headingKey(s.heading) === "scope");
23694
+ if (section === void 0) return void 0;
23695
+ return stripHtmlComments(section.lines).map((l) => l.text.trim().replace(/^[-*+]\s+/, "").trim()).map((t) => t.length >= 2 && t.startsWith("`") && t.endsWith("`") ? t.slice(1, -1) : t).map((t) => t.replace(/\\/g, "/")).filter((t) => t !== "");
23696
+ }
23697
+ const factOf = (s, id) => s.branchFacts !== void 0 && Object.hasOwn(s.branchFacts, id) ? s.branchFacts[id] : void 0;
23698
+ /** An entry matches a path when it ends in `/` and the path starts with it, or when the two are equal. No globs. */
23699
+ const matches = (entry, path) => entry.endsWith("/") ? path.startsWith(entry) : entry === path;
23700
+ /** Ledger Ruling 9: the ticket's own files, the files `accord skills sync` writes, and the merge-base config's `generated:` paths. */
23701
+ function isExempt(id, fact, path) {
23702
+ if (path === ticketFile$1(id) || path.startsWith("accord/tickets/" + id + "/")) return true;
23703
+ if (fact.synced.includes(path)) return true;
23704
+ if (path === ".claude/settings.json") return false;
23705
+ return configLists(fact.baseConfig).generated.some((e) => matches(e, path));
23706
+ }
23707
+ /** A change that only moves whitespace, blank lines or line endings has `lines: 0` and is never counted (D-02). */
23708
+ const isCounted = (c) => c.kind !== "modified" || c.binary === true || c.lines > 0;
23709
+ /** Lane S: the counted, non-exempt changed paths outside the recorded scope; `cannot` when no scope can be read. */
23710
+ function outsideScope(s, id) {
23711
+ if (laneTicket(s, id)?.frontmatter?.lane !== "S") return void 0;
23712
+ const fact = factOf(s, id);
23713
+ if (fact === void 0 || "error" in fact) return void 0;
23714
+ if (fact.scope === void 0) return { cannot: `no commit on this branch records ac_hash in ${ticketFile$1(id)}, so the ## Scope that gate ready fixed cannot be read; commit the ticket after accord gate ready ${id}` };
23715
+ const { commit, paths } = fact.scope;
23716
+ if (paths.length === 0) return { cannot: `## Scope held no path when ac_hash was first recorded (commit ${commit}); a lane S ticket lists the files it expects to touch before any change, so it must be lifted to lane M` };
23717
+ return { paths: fact.changes.filter((c) => isCounted(c) && !isExempt(id, fact, c.path) && !paths.includes(c.path)).map((c) => c.path).sort(cmp$6) };
23718
+ }
23719
+ const changesTicket = (fact, id) => fact.changes.some((c) => c.path === ticketFile$1(id));
23720
+ const hasChanges = (fact) => fact.changes.length > 0;
23721
+ const branchUnknown = (s, id) => {
23722
+ if (laneTicket(s, id) === void 0) return [];
23723
+ const file = ticketFile$1(id);
23724
+ const fact = factOf(s, id);
23725
+ if (fact === void 0) return [{
23726
+ file,
23727
+ reason: "the host loaded no branch diff for this ticket, so the files it changed cannot be checked"
23728
+ }];
23729
+ if ("error" in fact) return [{
23730
+ file,
23731
+ reason: fact.error
23732
+ }];
23733
+ if (!hasChanges(fact)) return [{
23734
+ file,
23735
+ reason: `the branch diff from ${fact.base}, the merge base with the default branch, to HEAD is empty: HEAD adds no change over the merge base; lane work is committed on the ticket's own branch, never on the default branch`
23736
+ }];
23737
+ if (laneTicket(s, id)?.frontmatter?.lane === "S" && !changesTicket(fact, id)) return [{
23738
+ file,
23739
+ reason: `the branch diff from ${fact.base}, the merge base with the default branch, does not change ${file}; lane work is committed on the ticket's own branch, never on the default branch`
23740
+ }];
23741
+ return [];
23742
+ };
23743
+ const outOfScope = (s, id) => {
23744
+ const fact = factOf(s, id);
23745
+ if (fact === void 0 || "error" in fact || !changesTicket(fact, id)) return [];
23746
+ const out = outsideScope(s, id);
23747
+ if (out === void 0) return [];
23748
+ const file = ticketFile$1(id);
23749
+ if ("cannot" in out) return [{
23750
+ file,
23751
+ reason: out.cannot
23752
+ }];
23753
+ const commit = fact.scope?.commit ?? "";
23754
+ const work = workOf(s, id);
23755
+ const declared = new Set((work?.ledger?.wordingOnly ?? []).map((w) => w.path));
23756
+ const confirmed = work?.review?.confirmed ?? [];
23757
+ return out.paths.flatMap((p) => {
23758
+ if (!declared.has(p)) return [{
23759
+ file,
23760
+ reason: `${p} is changed on this branch but not listed in ## Scope as recorded at commit ${commit}; the ticket must be lifted to lane M`
23761
+ }];
23762
+ const why = wordingOnlyFault(fact.changes.find((c) => c.path === p), p, confirmed, id);
23763
+ return why === void 0 ? [] : [{
23764
+ file,
23765
+ reason: `${p} is declared wording-only in ${ledgerFile(id)} but ${why}; the ticket must be lifted to lane M`
23766
+ }];
23767
+ });
23768
+ };
23769
+ /** The base entries of `sensitive:` then the HEAD entries, in key order, each (area, path) pair once. */
23770
+ function sensitiveEntries(s, fact) {
23771
+ const head = Object.entries(s.config?.sensitive ?? {}).flatMap(([area, paths]) => paths.map((path) => ({
23772
+ area,
23773
+ path
23774
+ })));
23775
+ const seen = /* @__PURE__ */ new Set();
23776
+ return [...configLists(fact.baseConfig).sensitive, ...head].filter((e) => {
23777
+ const key = e.area + "\0" + e.path;
23778
+ if (seen.has(key)) return false;
23779
+ seen.add(key);
23780
+ return true;
23781
+ });
23782
+ }
23783
+ /** Lane S: a counted change under a listed sensitive entry fails, whatever its scope, declaration or exemption (D-04, Ruling 29). */
23784
+ const sensitiveTouched = (s, id) => {
23785
+ if (laneTicket(s, id)?.frontmatter?.lane !== "S") return [];
23786
+ const fact = factOf(s, id);
23787
+ if (fact === void 0 || "error" in fact) return [];
23788
+ const entries = sensitiveEntries(s, fact);
23789
+ const file = ticketFile$1(id);
23790
+ return fact.changes.filter(isCounted).flatMap((c) => {
23791
+ const hit = entries.find((e) => matches(e.path, c.path));
23792
+ return hit === void 0 ? [] : [{
23793
+ file,
23794
+ reason: `${c.path} is in the sensitive area ${hit.area} (accord/config.yml lists ${hit.path} under sensitive); the ticket must be lifted to lane M`
23795
+ }];
23796
+ });
23797
+ };
23798
+ /** Ledger Ruling 13: an added line that calls a commander-style `command` or `option`. */
23799
+ const CALL_SIGN = /\.(command|option)\(/;
23800
+ /** The keys of the `exports` or `bin` map of a package.json text; a string value counts as the key `.`. */
23801
+ function keysOf(text, map) {
23802
+ if (text === void 0) return [];
23803
+ try {
23804
+ const value = JSON.parse(text)?.[map];
23805
+ if (typeof value === "string") return ["."];
23806
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : [];
23807
+ } catch {
23808
+ return [];
23809
+ }
23810
+ }
23811
+ const isPackageJson = (path) => path === "package.json" || path.endsWith("/package.json");
23812
+ /** Ledger Ruling 20: a call sign is read only in a non-Markdown file the ticket's own exemptions do not cover. */
23813
+ const callsOf = (c, id, fact) => c.calls === void 0 || /\.(md|markdown)$/i.test(c.path) || isExempt(id, fact, c.path) ? [] : [...new Set(c.calls.flatMap((l) => [...l.matchAll(new RegExp(CALL_SIGN.source, "g"))].map((m) => m[1])))];
23814
+ /** D-04: signs of a new interface in paths no sensitive entry matches; undefined without a lane ticket or a readable diff. */
23815
+ function interfaceSigns(s, id) {
23816
+ if (laneTicket(s, id) === void 0) return void 0;
23817
+ const fact = factOf(s, id);
23818
+ if (fact === void 0 || "error" in fact) return void 0;
23819
+ const entries = sensitiveEntries(s, fact);
23820
+ const out = [];
23821
+ for (const c of fact.changes) {
23822
+ if (entries.some((e) => matches(e.path, c.path))) continue;
23823
+ const add = (sign) => out.push({
23824
+ path: c.path,
23825
+ sign
23826
+ });
23827
+ if (c.kind === "added" && (c.path.split("/").slice(0, -1).includes("schemas") || c.path.endsWith(".schema.json"))) add("a new schema file");
23828
+ if (isPackageJson(c.path)) for (const map of ["exports", "bin"]) {
23829
+ const before = keysOf(c.pkg?.base, map);
23830
+ keysOf(c.pkg?.head, map).filter((k) => !before.includes(k)).forEach((k) => add(`a new key "${k}" in the ${map} map of package.json`));
23831
+ }
23832
+ callsOf(c, id, fact).forEach((name) => add(`an added line that calls .${name}(`));
23833
+ }
23834
+ return out.sort((a, b) => cmp$6(a.path, b.path) || cmp$6(a.sign, b.sign));
23835
+ }
23836
+ const interfaceSign = (s, id) => {
23837
+ const signs = interfaceSigns(s, id);
23838
+ if (signs === void 0) return [];
23839
+ const file = ticketFile$1(id);
23840
+ if (laneTicket(s, id)?.frontmatter?.lane === "S") return signs.map(({ path, sign }) => ({
23841
+ file,
23842
+ reason: `${path} adds ${sign}, a sign of a new interface; the ticket must be lifted to lane M`
23843
+ }));
23844
+ const excused = workOf(s, id)?.review?.notInterface ?? [];
23845
+ return signs.filter(({ path }) => !excused.includes(path)).map(({ path, sign }) => ({
23846
+ file,
23847
+ reason: `${path} adds ${sign}, a sign of a new interface no sensitive entry matches; add an entry matching it under sensitive: in accord/config.yml, or have the reviewer record "Not an interface: ${path} — <reason>" in the last ## Round of ${reviewFile(id)}`
23848
+ }));
23849
+ };
23850
+ /** D-02, D-07: why a declared wording-only file does not qualify, or undefined when it does. */
23851
+ function wordingOnlyFault(c, path, confirmed, id) {
23852
+ if (c?.kind === "added") return "is new";
23853
+ if (c?.kind === "deleted") return "is deleted";
23854
+ if (c?.kind === "type" || c?.kind === "mode") return "changes its file type or mode";
23855
+ if (c?.binary === true) return "is binary";
23856
+ if (c !== void 0 && c.lines > 3) return `changes ${c.lines} lines, more than 3`;
23857
+ if (!confirmed.includes(path)) return `has no "Confirmed wording-only: ${path}" line in the last ## Round of ${reviewFile(id)}`;
23858
+ }
23596
23859
  const EXT = new Set(`ts tsx js jsx mjs cjs json yml yaml md html css scss sql py go rs java rb php sh xml toml txt vue
23597
23860
  svelte`.split(/\s+/));
23598
23861
  const PUNCT = "`'\"“”‘’()[]{}<>,;:.!?";
@@ -23681,8 +23944,8 @@ function stripRefs(text, snapshot) {
23681
23944
  }
23682
23945
  /**
23683
23946
  * D-85 normalisation: lower case, punctuation and symbols to a space, whitespace collapsed. Combining
23684
- * marks and precomposed letters are left alone — content in Vietnamese is the expected case, and folding
23685
- * diacritics would make `trả` and `tra` the same word and manufacture a false match.
23947
+ * marks and precomposed letters are left alone — content in a language with diacritics, such as French, is
23948
+ * the expected case, and folding them would make `où` and `ou` the same word and manufacture a false match.
23686
23949
  */
23687
23950
  function normalise(text) {
23688
23951
  return text.toLowerCase().replace(/[\p{P}\p{S}]/gu, " ").replace(/\s+/g, " ").trim();
@@ -23991,6 +24254,20 @@ const COMMIT_TRAILER = {
23991
24254
  profiles: ["build", "maintain"],
23992
24255
  check: commitTrailer
23993
24256
  };
24257
+ /** ST-LN-62: Done reads the branch diff of every ticket with `lane:`; no diff, an empty diff, or in lane S a diff without the ticket file, fails closed. */
24258
+ const BRANCH_UNKNOWN = {
24259
+ id: "gate.branch-unknown",
24260
+ level: "error",
24261
+ profiles: ["build", "maintain"],
24262
+ check: branchUnknown
24263
+ };
24264
+ /** ST-LN-62 D-04: a sign of a new interface in a path no sensitive entry matches, in every lane that carries `lane:`. */
24265
+ const INTERFACE_SIGN = {
24266
+ id: "gate.interface-sign",
24267
+ level: "error",
24268
+ profiles: ["build", "maintain"],
24269
+ check: interfaceSign
24270
+ };
23994
24271
  /** GATE-02/GATE-03/GATE-11 (docs/design.md §5). Every row is an error on both profiles: Done is the
23995
24272
  * gate that must not be talked past, and GATE-06 forbids a level that depends on a flag. */
23996
24273
  const DONE_RULES = [
@@ -24068,6 +24345,8 @@ const DONE_RULES = [
24068
24345
  check: staleReview
24069
24346
  },
24070
24347
  COMMIT_TRAILER,
24348
+ BRANCH_UNKNOWN,
24349
+ INTERFACE_SIGN,
24071
24350
  {
24072
24351
  id: "gate.evidence-unresolved",
24073
24352
  level: "error",
@@ -24201,7 +24480,21 @@ const DONE_S_RULES = [
24201
24480
  check: commitAfterReview
24202
24481
  },
24203
24482
  SUITE_NOT_PASSED,
24204
- COMMIT_TRAILER
24483
+ COMMIT_TRAILER,
24484
+ BRANCH_UNKNOWN,
24485
+ {
24486
+ id: "gate.out-of-scope",
24487
+ level: "error",
24488
+ profiles: ["build", "maintain"],
24489
+ check: outOfScope
24490
+ },
24491
+ {
24492
+ id: "gate.sensitive-touched",
24493
+ level: "error",
24494
+ profiles: ["build", "maintain"],
24495
+ check: sensitiveTouched
24496
+ },
24497
+ INTERFACE_SIGN
24205
24498
  ];
24206
24499
  /** D-89: the three LINT-06 hygiene rules Ready promotes from warning to blocking. */
24207
24500
  const READY_PROMOTE = [
@@ -24528,6 +24821,7 @@ function anchorText(snapshot, id) {
24528
24821
  "Understood",
24529
24822
  "Done when",
24530
24823
  ...[
24824
+ "Scope",
24531
24825
  "Vetoes",
24532
24826
  "Decisions",
24533
24827
  "Corrections"
@@ -24733,7 +25027,7 @@ const templates = {
24733
25027
  "prototype-header.html": "<!--\naccord prototype for ticket TICKET-ID\nDerived from: <list the stylesheets or token files this prototype was built from, one per line>\nRule: use only colours and spacing from those files; the token rule (lint) warns on hard-coded values\nOwner: designer (assets/<id>/ is designer-owned)\n-->\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>TICKET-ID prototype</title>\n</head>\n<body>\n</body>\n</html>\n",
24734
25028
  "ruleset.md": "# accord rules\n\nThese hold for every reply here, for the main session and every accord agent.\n\n1. Ask about intent, never guess it. A question that touches the intent, a veto, an acceptance criterion, or\n a public interface stops the work until the owner answers. Any other decision you take is a Ruling,\n `Ruling: <what> — <why> — <cost if wrong>`. The main session writes it to the ticket's ledger; an accord\n agent lists its Rulings in its report and never writes the ledger unless its own instructions say so.\n2. When the owner corrects you, write the correction verbatim under the ticket's `## Corrections` before\n doing anything else. A correction that exists only in the conversation is lost at the next compaction.\n3. Scope is impact: every caller, test, fixture, config entry, and export the change must reach. Anything\n beyond that is extra, and extra is not done.\n4. Take the lightest change that fully works: does it need to exist; is it already in the repository; the\n standard library or platform; an installed dependency; then the minimum new code. Never cut validation at\n trust boundaries, data-loss handling, security, accessibility, or anything the owner asked for.\n5. Evidence before claims. Say something works only after running what proves it, and show the output. Whoever\n made a change never grades it.\n6. Tests: one behaviour test per acceptance criterion, named by its `@test:<id>` tag, written first, at the\n cheapest layer that observes the behaviour. Any other test names a risk with `risk:<kind>`. No test that\n restates the implementation or only asserts on mocks.\n7. End every reply with one or two lines: what was skipped, what was not checked, and any risk the owner\n must know. When your instructions fix the shape of your report, these lines go inside that report, within\n its limits.\n8. Every commit, by the main session or any agent, follows `accord/product/conventions.md` when that file exists.\n\nRun accord as `{{invocation}}` when `accord` is not on PATH.\n",
24735
25029
  "ticket-build.md": "---\nid: \"TICKET-ID\" # equals the file name: tickets/<id>.md\ntitle: \"Short title in business language\"\ntype: story # epic | story | bug. story and bug touch code and need at least one tagged scenario\nstatus: draft # draft | open | archived | dropped. Document lifecycle only; work status stays in the tracker. draft blocks Ready\n# The epic this ticket belongs to. Grouping is by parent; there is no per-epic folder.\n# parent: \"EPIC-ID\"\n# Ids in the tracker, a map keyed by adapter name; values are strings.\n# tracker: { github-issues: \"1234\" }\n# When true, Ready requires a design reference; the Intent note below says which one this profile expects.\nui: false\n# Optional design reference URL. Allowed on epics too.\n# design: \"https://example.com/design/...\"\n# Unconfirmed assumptions block Ready.\n# assumptions:\n# - { text: \"...\", confirmed: false }\n# Written by `gate ready`; never edit by hand.\n# ac_hash:\n# Developer self-test ticks, e.g. [ac-1, ac-2]. Only the developer writes it.\n# Always the last key so developer hunks stay far from BA hunks.\n# verified: []\n# The AC hash the ticks above were made against, copied from ac_hash at the moment of ticking.\n# verified_hash:\n# The commit sha those ticks were made against. Done fails unless both still equal the current values; a human writes both by hand.\n# verified_commit:\n---\n\n## Intent\n<!-- BA. Why, for whom, how success is measured, and explicit non-goals. About five lines.\nDescribe observable behaviour in business language; never name tables, endpoints, libraries, or screens.\nTerms go in product/glossary.md; rules already decided go in product/business-rules.md.\nIf the story has a screen, set ui: true in the frontmatter. Ready then requires a design link in design:. -->\n\n## Requirements\n<!-- BA. One EARS line per requirement: WHEN <trigger> the system SHALL <response>, and its siblings. 5 to 15 lines. -->\n\n## Acceptance criteria\n<!-- BA. One fenced gherkin block; every scenario tagged @ac-n with a unique n; 2 to 5 scenarios.\nA bug is written as Given the situation, When the action, Then the correct result.\nA scenario that is not @ui also carries one @test:<id> tag, where <id> is the case id from the test report (classname#name, spaces as -); add it after Ready, it is outside the AC hash. -->\n```gherkin\nFeature: TICKET-ID\n\n @ac-1\n Scenario: <observable outcome>\n Given ...\n When ...\n Then ...\n```\n\n## Open questions\n<!-- BA. `- [ ]` items; Ready stays blocked while any is unchecked. -->\n\n## Plan\n<!-- Developer fills this in. BA leaves it empty.\nA short plan only. Evidence goes to tickets/<id>/verification.md, written by the fresh review context, not by the agent that wrote the code. -->\n\n## Verification notes\n<!-- Developer, at Done. One \"### @ac-n <scenario name>\" block per ticked scenario, a few lines each: what was run and what was seen. This section stays last; evidence with a result lives in tickets/<id>/verification.md. -->\n",
24736
- "ticket-lane-s.md": "---\nid: \"TICKET-ID\" # equals the file name: tickets/<id>.md\ntitle: \"Short title in business language\"\ntype: story # story | bug\nstatus: draft # draft | open | archived | dropped. Document lifecycle only; work status stays in the tracker\nlane: S # S: one small change, worked from this ticket alone\n# Tickets that must reach done before this one can start.\n# depends_on: [\"OTHER-ID\"]\n# How the dashboard ranks this ticket: high | medium | low; absent means medium.\n# priority: medium\n# Ids in the tracker, a map keyed by adapter name; values are strings.\n# tracker: { github-issues: \"1234\" }\nui: false\n# Written by `gate ready`; never edit by hand.\n# ac_hash:\n---\n\n## Understood\n<!-- One line: the change, as the request was understood. -->\n\n## Done when\n<!-- One line: the observable result that ends the work. -->\n",
25030
+ "ticket-lane-s.md": "---\nid: \"TICKET-ID\" # equals the file name: tickets/<id>.md\ntitle: \"Short title in business language\"\ntype: story # story | bug\nstatus: draft # draft | open | archived | dropped. Document lifecycle only; work status stays in the tracker\nlane: S # S: one small change, worked from this ticket alone\n# Tickets that must reach done before this one can start.\n# depends_on: [\"OTHER-ID\"]\n# How the dashboard ranks this ticket: high | medium | low; absent means medium.\n# priority: medium\n# Ids in the tracker, a map keyed by adapter name; values are strings.\n# tracker: { github-issues: \"1234\" }\nui: false\n# Written by `gate ready`; never edit by hand.\n# ac_hash:\n---\n\n## Understood\n<!-- One line: the change, as the request was understood. -->\n\n## Done when\n<!-- One line: the observable result that ends the work. -->\n\n## Scope\n<!-- One repository path per line, never a folder: every file the change is expected to touch, tests and new files included. -->\n",
24737
25031
  "ticket-lane.md": "---\nid: \"TICKET-ID\" # equals the file name: tickets/<id>.md\ntitle: \"Short title in business language\"\ntype: story # story | bug. An epic uses its own template\nstatus: draft # draft | open | archived | dropped. Document lifecycle only; work status stays in the tracker\nlane: M # M: one feature | L: a story of an epic | P: part of a whole project\n# Tickets that must reach done before this one can start.\n# depends_on: [\"OTHER-ID\"]\n# How the dashboard ranks this ticket: high | medium | low; absent means medium.\n# priority: medium\n# The epic this ticket belongs to.\n# parent: \"EPIC-ID\"\n# Ids in the tracker, a map keyed by adapter name; values are strings.\n# tracker: { github-issues: \"1234\" }\nui: false\n# Optional design reference URL.\n# design: \"https://example.com/design/...\"\n# Written by `accord approve` after the owner says yes; never edit by hand.\n# approved_hash:\n# Written by `gate ready`; never edit by hand.\n# ac_hash:\n# Self-test ticks, e.g. [ac-1, ac-2]. Always the last key.\n# verified: []\n# The AC hash and the commit the ticks were made against.\n# verified_hash:\n# verified_commit:\n---\n\n## Intent\n<!-- At most five lines: why, for whom, how success is measured, and what this does not do. -->\n\n## Acceptance criteria\n<!-- One fenced gherkin block; every scenario tagged @ac-n with a unique n; 2 to 5 scenarios.\nA scenario that is not @ui also carries one @test:<id> tag naming the one test that proves it; `accord task-done` writes it from the plan's `(@ac-n)` marker, not by hand; it is outside the AC hash. -->\n```gherkin\nFeature: TICKET-ID\n\n @ac-1\n Scenario: <observable outcome>\n Given ...\n When ...\n Then ...\n```\n\n## Vetoes\n<!-- What the owner forbids, in the owner's own words, one item per line. -->\n\n## Decisions\n<!-- Settled answers, one per line: - D-01 <decision> — \"<the owner's sentence>\" -->\n\n## Corrections\n<!-- Every correction the owner makes, verbatim, appended and never edited. -->\n\n## Open questions\n<!-- `- [ ]` items; Ready stays blocked while any is unchecked. -->\n\n## Approach\n<!-- About fifteen lines: the chosen option and why, rejected options and why, affected areas, risks, and which criterion is tested at which layer. -->\n",
24738
25032
  "ticket-maintain.md": "---\nid: \"TICKET-ID\" # equals the file name: tickets/<id>.md\ntitle: \"Short title in business language\"\ntype: story # epic | story | bug. story and bug touch code and need at least one tagged scenario\nstatus: draft # draft | open | archived | dropped. Document lifecycle only; work status stays in the tracker. draft blocks Ready\n# The epic this ticket belongs to. Grouping is by parent; there is no per-epic folder.\n# parent: \"EPIC-ID\"\n# Ids in the tracker, a map keyed by adapter name; values are strings.\n# tracker: { github-issues: \"1234\" }\n# When true, Ready requires a design reference; the Intent note below says which one this profile expects.\nui: false\n# Optional design reference URL. Allowed on epics too.\n# design: \"https://example.com/design/...\"\n# Unconfirmed assumptions block Ready.\n# assumptions:\n# - { text: \"...\", confirmed: false }\n# Written by `gate ready`; never edit by hand.\n# ac_hash:\n# Developer self-test ticks, e.g. [ac-1, ac-2]. Only the developer writes it.\n# Always the last key so developer hunks stay far from BA hunks.\n# verified: []\n# The AC hash the ticks above were made against, copied from ac_hash at the moment of ticking.\n# verified_hash:\n# The commit sha those ticks were made against. Done fails unless both still equal the current values; a human writes both by hand.\n# verified_commit:\n---\n\n## Intent\n<!-- BA. Why, for whom, how success is measured, and explicit non-goals. About five lines.\nDescribe observable behaviour in business language; never name tables, endpoints, libraries, or screens.\nTerms go in product/glossary.md; rules already decided go in product/business-rules.md.\nIf the story has a screen, set ui: true in the frontmatter. Ready then requires a prototype at assets/<id>/prototype.html derived from the project's existing styles.\nA design link in design: is optional in maintain; the prototype is the primary reference. -->\n\n## Requirements\n<!-- BA. One EARS line per requirement: WHEN <trigger> the system SHALL <response>, and its siblings. 5 to 15 lines. -->\n\n## Acceptance criteria\n<!-- BA. One fenced gherkin block; every scenario tagged @ac-n with a unique n; 2 to 5 scenarios.\nA bug is written as Given the situation, When the action, Then the correct result. A bug fix still needs at least one scenario.\nA scenario that is not @ui also carries one @test:<id> tag, where <id> is the case id from the test report (classname#name, spaces as -); add it after Ready, it is outside the AC hash. -->\n```gherkin\nFeature: TICKET-ID\n\n @ac-1\n Scenario: <observable outcome>\n Given ...\n When ...\n Then ...\n```\n\n## Open questions\n<!-- BA. `- [ ]` items; Ready stays blocked while any is unchecked. -->\n\n## Plan\n<!-- Developer fills this in. BA leaves it empty.\nA short plan only. Evidence goes to tickets/<id>/verification.md, written by the fresh review context, not by the agent that wrote the code. -->\n\n## Verification notes\n<!-- Developer, at Done. One \"### @ac-n <scenario name>\" block per ticked scenario, a few lines each: what was run and what was seen. This section stays last; evidence with a result lives in tickets/<id>/verification.md. -->\n",
24739
25033
  "verification.md": "---\nticket: \"TICKET-ID\"\ncommit: \"0000000\"\nreviewed_on: \"2026-01-01\"\n---\n\n<!-- This file is tickets/<id>/verification.md. It is written only by a verifier in a fresh context, never by the agent that wrote the code.\nOne `## @ac-n <scenario name>` block per scenario. Matching is by tag; the name is for humans. -->\n\n## @ac-1 <scenario name>\n\nResult: pass\n<!-- pass | fail | blocked -->\n\nEvidence: <what was run or inspected, may span several lines>\n"
@@ -24753,9 +25047,6 @@ function sessionLine(rows) {
24753
25047
  const ready = nextRows(rows);
24754
25048
  return `accord: ${lane.filter((r) => r.stage !== void 0 && WORKING.includes(r.stage) && !ready.includes(r)).length} in progress, ${ready.length} ready to pick up · type /accord`;
24755
25049
  }
24756
- const TEAM_CAP = 10;
24757
- const READY_CAP = 20;
24758
- const EPIC_CAP = 20;
24759
25050
  const closed = (r) => r.status === "archived" || r.status === "dropped";
24760
25051
  const isWork = (r) => r.lane !== void 0 && r.type !== "epic";
24761
25052
  const isOpen = (r) => isWork(r) && !closed(r) && r.stage !== "done";
@@ -24806,30 +25097,28 @@ function effectivePriorities(snapshot, plain) {
24806
25097
  }
24807
25098
  return out;
24808
25099
  }
24809
- function section(rows, cap, prio) {
25100
+ function section(rows, prio) {
24810
25101
  const p = (r) => prio.get(r.id) ?? 2;
24811
25102
  const rank = (a, b) => p(b) - p(a) || Number(blocked(a)) - Number(blocked(b)) || cmpId(a.id, b.id);
24812
- const sorted = [...rows].sort(rank);
24813
25103
  return {
24814
- rows: sorted.slice(0, cap),
24815
- more: Math.max(0, sorted.length - cap)
25104
+ rows: [...rows].sort(rank),
25105
+ more: 0
24816
25106
  };
24817
25107
  }
24818
25108
  function epicsSection(snapshot, plain) {
24819
25109
  const tickets = Object.entries(snapshot.tickets);
24820
- const rows = tickets.filter(([, t]) => t.frontmatter?.type === "epic" && t.frontmatter.status !== "archived" && t.frontmatter.status !== "dropped").map(([id, t]) => {
24821
- const kids = tickets.filter(([, k]) => k.frontmatter?.parent === id && k.frontmatter.status !== "dropped");
24822
- return {
24823
- id,
24824
- ...t.frontmatter?.lane === void 0 ? {} : { lane: t.frontmatter.lane },
24825
- title: t.frontmatter?.title ?? "",
24826
- done: kids.filter(([k]) => isDone(plain, k)).length,
24827
- total: kids.length
24828
- };
24829
- }).sort((a, b) => Number(a.total > 0 && a.done === a.total) - Number(b.total > 0 && b.done === b.total) || cmpId(a.id, b.id));
24830
25110
  return {
24831
- rows: rows.slice(0, EPIC_CAP),
24832
- more: Math.max(0, rows.length - EPIC_CAP)
25111
+ rows: tickets.filter(([, t]) => t.frontmatter?.type === "epic" && t.frontmatter.status !== "archived" && t.frontmatter.status !== "dropped").map(([id, t]) => {
25112
+ const kids = tickets.filter(([, k]) => k.frontmatter?.parent === id && k.frontmatter.status !== "dropped");
25113
+ return {
25114
+ id,
25115
+ ...t.frontmatter?.lane === void 0 ? {} : { lane: t.frontmatter.lane },
25116
+ title: t.frontmatter?.title ?? "",
25117
+ done: kids.filter(([k]) => isDone(plain, k)).length,
25118
+ total: kids.length
25119
+ };
25120
+ }).sort((a, b) => Number(a.total > 0 && a.done === a.total) - Number(b.total > 0 && b.done === b.total) || cmpId(a.id, b.id)),
25121
+ more: 0
24833
25122
  };
24834
25123
  }
24835
25124
  function dashboard(snapshot, viewer, now, plainSnapshot) {
@@ -24841,17 +25130,17 @@ function dashboard(snapshot, viewer, now, plainSnapshot) {
24841
25130
  const claimed = open.filter((r) => r.claimedBy !== void 0);
24842
25131
  const readyAll = nextRows(open);
24843
25132
  const unclaimed = open.filter((r) => r.claimedBy === void 0);
24844
- const yours = section(claimed.filter(mine), Infinity, prio);
24845
- const ready = section(readyAll, READY_CAP, prio);
24846
- const rest = section(unclaimed.filter((r) => !readyAll.includes(r)), Infinity, prio).rows;
25133
+ const yours = section(claimed.filter(mine), prio);
25134
+ const ready = section(readyAll, prio);
25135
+ const rest = section(unclaimed.filter((r) => !readyAll.includes(r)), prio).rows;
24847
25136
  const first = rest.find((r) => !blocked(r));
24848
25137
  return {
24849
25138
  yours,
24850
- team: section(claimed.filter((r) => !mine(r)), TEAM_CAP, prio),
25139
+ team: section(claimed.filter((r) => !mine(r)), prio),
24851
25140
  ready,
24852
25141
  upNext: {
24853
- rows: rest.slice(0, READY_CAP),
24854
- more: Math.max(0, rest.length - READY_CAP),
25142
+ rows: rest,
25143
+ more: 0,
24855
25144
  ...first ? { suggest: {
24856
25145
  action: "pick-up",
24857
25146
  id: first.id
@@ -24948,25 +25237,27 @@ function markDropped(text, reason) {
24948
25237
  return (bom ? "" : "") + out;
24949
25238
  }
24950
25239
  const skills$1 = {
24951
- "accord/SKILL.md": "---\nname: accord\ndescription: Use when starting any work in an accord repository, when seeing where work stands, or when continuing a ticket. Shows the dashboard, continues or picks up a ticket, and classifies a new request into a lane.\nlicense: MIT\nkind: skill\nloads: [accord/ids.md, accord/lane-s.md, accord/upgrade.md, accord/worktree.md, shared/ask.md, shared/failure.md]\n---\n\nFirst run the session check in `./worktree.md`, except for `/accord upgrade`. A failure reported in a ticket that has reached verify follows `./failure.md`.\n\n## Dashboard: `/accord` with no argument\n\n1. Run `accord worktree clean`, then `accord dashboard`. It reads you from\n `git config user.email` and prints every section ranked and capped, one\n count line, and the actions that apply. When it exits 2, show its message\n and stop.\n2. Show both outputs verbatim in a code block. Never reorder, cut, merge, or\n add to it, and never build a section from `accord status --json`.\n3. Offer the actions its last line names: Continue, Pick up, New request.\n\n## A ticket named: `/accord pick-up <id>`, `/accord continue <id>`\n\nAn action applies only when the whole text after it is exactly one\nticket id, the id of a ticket in `accord/tickets/`, an id that\n`accord status --json` lists with a `branch`, or for Continue a ticket\n`accord worktree open <id> --continue` finds; with none,\n`/accord continue ST-99` is a New request. Then run Pick up or\nContinue below on that ticket, without showing the dashboard first. Continue on a ticket nobody has claimed\nsays so, offers Pick up, and never claims it. Any other text, but `upgrade`, starts a\nNew request whose text is the whole text after `/accord`: for\n`/accord continue the refactor` it is `continue the refactor`, and\n`/accord continue ST-2 now` is a new request too.\n\n## Continue\n\n1. Run `accord worktree open <id> --continue` unless it just ran; follow\n `./worktree.md`. The active ticket is its `branch` from\n `accord status --json`, read from git, never from memory.\n2. Run `accord anchor`.\n3. If `accord/tickets/<id>/debug.md` exists and its last `## ` heading is\n not `## Resolved`, load `accord-debug` and resume there.\n4. Otherwise, when it prints `Lane: S`, follow `./lane-s.md`. Else load the stage skill\n for the stage it prints: `intent` →\n `accord-approach` at step 5, the owner's yes, when the ticket's\n `## Approach` is already written, otherwise `accord-intent`; `plan` →\n `accord-plan`, `build` → `accord-build`, `review` → `accord-review`,\n `verify` → `accord-verify`; `done` → `accord-verify` at step 6.\n Resume at the step the anchor names. Never re-run a task\n the ledger records.\n\n## Pick up\n\nRun `accord worktree open <id>`: it makes its `branch` from\n`accord status --json` and commits `claim: <id>` on it. Follow\n`./worktree.md`; it says when to push it (the pushed branch is the claim).\n\n## New request: `/accord <request>`\n\nPropose a lane with its reason, out loud. The owner may override.\n\n| Lane | Example |\n|---|---|\n| S: small fix | bug, copy change, one flag |\n| M: feature | one story |\n| L: epic | 3 to 10 related stories |\n| P: project | greenfield |\n\nAny of these lifts S to M, and nothing else: a public interface or schema\nchanges; auth, money, or data deletion is touched; more than one plausible\nreading; more than about five hand-written files expected, generated and\nrendered copies not counted.\n\n- **S.** Run `accord new ticket <id> --lane S --worktree`; when config.yml has\n `people`, follow `./ids.md` and run it with `--type <story|bug>` and no id\n instead. Then follow `./lane-s.md`. A request that carries `Understood: …`\n and `Done when: …` from a drop is lane S without asking again.\n- **M.** Run `accord new ticket <id> --lane M --worktree`; when config.yml has\n `people`, follow `./ids.md` and run `accord new ticket --type <story|bug>\n --lane M --worktree` with no id instead. It writes the ticket on its\n `branch` from `accord status --json`, commits it and pushes it; follow\n `./worktree.md`, and push it as that file says when the push fails.\n- **L and P.** Not built yet. Say so, and propose splitting the work into\n lane M tickets linked by `depends_on`.\n\n## Upgrade: `/accord upgrade`\n\nFollow `./upgrade.md`.\n\n## Rules\n\n- A lane only rises. Complexity found mid-task lifts the lane: stop, say so,\n and re-enter at the new lane's first incomplete stage. A lane never drops once its intent is approved; before\n that only the intent interview's move to lane S drops it.\n- A ticket without `lane:` follows the 0.1.0 contract. Offer to add `lane: M`\n and the lane headings; do it only on a yes.\n- In lane M the main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision on a\n ticket is a Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`.\n Read `./ask.md` before any question to the owner.\n",
25240
+ "accord/SKILL.md": "---\nname: accord\ndescription: Use when starting any work in an accord repository, when seeing where work stands, or when continuing a ticket. Shows the dashboard, continues or picks up a ticket, and classifies a new request into a lane.\nlicense: MIT\nkind: skill\nloads: [accord/ids.md, accord/lane.md, accord/lane-s.md, accord/upgrade.md, accord/worktree.md, shared/ask.md, shared/failure.md, shared/finish.md]\n---\n\nFirst run the session check in `./worktree.md`, except for `/accord upgrade`. A failure reported in a ticket that has reached verify follows `./failure.md`.\n\n## Dashboard: `/accord` with no argument\n\n1. Run `accord worktree clean`, then `accord dashboard`. It reads you from\n `git config user.email` and prints every section ranked, one\n count line, and the actions that apply. When it exits 2, show its message\n and stop.\n2. Show both outputs verbatim in a code block. Never reorder, cut, merge, or\n add to it, and never build a section from `accord status --json`.\n3. Offer the actions its last line names: Continue, Pick up, New request.\n\n## A ticket named: `/accord pick-up <id>`, `/accord continue <id>`\n\nAn action applies only when the whole text after it is exactly one\nticket id, the id of a ticket in `accord/tickets/`, an id that\n`accord status --json` lists with a `branch`, or for Continue a ticket\n`accord worktree open <id> --continue` finds; with none,\n`/accord continue ST-99` is a New request. Then run Pick up or\nContinue below on that ticket, without showing the dashboard first. Continue on a ticket nobody has claimed\nsays so, offers Pick up, and never claims it. Any other text, but `upgrade`, starts a\nNew request whose text is the whole text after `/accord`: for\n`/accord continue the refactor` it is `continue the refactor`, and\n`/accord continue ST-2 now` is a new request too.\n\n## Continue\n\n1. Run `accord worktree open <id> --continue` unless it just ran; follow\n `./worktree.md`. The active ticket is its `branch` from\n `accord status --json`, read from git, never from memory.\n2. Run `accord anchor`.\n3. If `accord/tickets/<id>/debug.md` exists and its last `## ` heading is\n not `## Resolved`, load `accord-debug` and resume there.\n4. Otherwise, when it prints `Lane: S`, follow `./lane-s.md`. Else load the stage skill\n for the stage it prints: `intent` →\n `accord-approach` at step 5, the owner's yes, when the ticket's\n `## Approach` is already written, otherwise `accord-intent`; `plan` →\n `accord-plan`, `build` → `accord-build`, `review` → `accord-review`,\n `verify` → `accord-verify`; `done` → `accord-verify` at step 6.\n Resume at the step the anchor names. Never re-run a task\n the ledger records.\n\n## Pick up\n\nRun `accord worktree open <id>`: it makes its `branch` from\n`accord status --json` and commits `claim: <id>` on it. Follow\n`./worktree.md`; it says when to push it (the pushed branch is the claim).\n\n## New request: `/accord <request>`\n\nPropose a lane as `./lane.md` says, for every new request however small it looks; the owner may override.\n\n| Lane | Example |\n|---|---|\n| S: small fix | bug, copy change, one flag |\n| M: feature | one story |\n| L: epic | 3 to 10 related stories |\n| P: project | greenfield |\n\n- **S.** Run `accord new ticket <id> --lane S --worktree`; when config.yml has\n `people`, follow `./ids.md` and run it with `--type <story|bug>` and no id\n instead. Then follow `./lane-s.md`. A request that carries `Understood: …`\n and `Done when: …` from a drop is lane S without asking again; the scout still runs first and its\n files still go under `## Scope`.\n- **M.** Run `accord new ticket <id> --lane M --worktree`; when config.yml has\n `people`, follow `./ids.md` and run `accord new ticket --type <story|bug>\n --lane M --worktree` with no id instead. It writes the ticket on its\n `branch` from `accord status --json`, commits it and pushes it; follow\n `./worktree.md`, and push it as that file says when the push fails.\n- **L and P.** Not built yet. Say so, and propose splitting the work into\n lane M tickets linked by `depends_on`.\n\n## Upgrade: `/accord upgrade`\n\nFollow `./upgrade.md`.\n\n## Rules\n\n- A lane only rises. Complexity found mid-task lifts the lane: stop, say so,\n and re-enter at the new lane's first incomplete stage. A lane never drops once its intent is approved; before\n that only the intent interview's move to lane S drops it.\n- A ticket without `lane:` follows the 0.1.0 contract. Offer to add `lane: M`\n and the lane headings; do it only on a yes.\n- In lane M the main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision on a\n ticket is a Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`.\n Read `./ask.md` before any question to the owner.\n",
24952
25241
  "accord/ids.md": "---\nkind: reference\n---\n\n# Ticket ids, people, and dropping\n\nRead this when config.yml has `people`. Register only when `people` exists, never create it.\n\n## Assigning\n\nFetch first with\n`git -c credential.interactive=never -c core.sshCommand=\"ssh -o BatchMode=yes\" fetch`;\non failure say so and continue, since only refs already in the clone are read.\nThen run `accord new ticket --type <story|bug> --lane <lane>` with no id, adding `--worktree` when the lane is M or S, and use the id it prints.\nNever retry with an id.\n\n## When your email is not registered\n\nLook for it on other branches with\n`git -c credential.interactive=never -c core.sshCommand=\"ssh -o BatchMode=yes\" log --all -S \"<email>\" --oneline -- accord/config.yml`\nand `git show <commit>:accord/config.yml` (the same prefix, since reading blobs can fetch in a partial clone),\nand reuse those initials. If that lookup fails or finds nothing, ask the owner for initials, 2 to 4 capital\nletters, suggested from `git config user.name`. Add `\"<email>\": <INITIALS>` under `people`; when the file has\n`people: {}`, write `people:` with the entry on the next line, indented two spaces, in its place.\nRecognition names tickets and is never a gate. When the command says the initials belong to someone else,\nask for other initials.\n\n## Merge conflicts under `people`\n\nKeep each email once, comparing emails without regard to case. When the two sides give one email different\ninitials, or two different emails the same initials, ask the owner which to keep.\n\n## Dropping instead of renaming\n\nA ticket's type never changes and its id is never renamed. To change the type:\n\n1. When the ticket has a worktree, first commit every change in the ticket's worktree on its claim branch and\n push it with `git push -u origin <claim branch>`, so that the next `accord worktree clean` finds nothing that\n exists only there. Then run `accord drop <id> --reason \"<reason>\"` with the owner's reason. It pushes a branch\n made from the default branch that holds only the ticket file, set to dropped with that reason, and prints\n `branch:`, `base:` and `title:` lines. Open that pull request with\n `gh pr create --head <branch> --base <base> --title \"<title>\" --body \"<reason>\"` when `gh` is available,\n otherwise ask the owner to open it; it waits for the owner's merge like other ticket work. Tell the owner to\n close this session once the pull request is opened, because `accord worktree clean` keeps a worktree that a\n live session holds. Never delete a claim branch to drop a ticket: once the pull request is merged,\n `accord worktree clean` deletes it and its worktree.\n2. Create the new ticket with `accord new ticket --type <the other type> --lane <lane>`, always passing\n `--lane`: the dropped ticket's lane, except `L` for a new epic and `M` for a story or bug made from an epic.\n3. Copy into it the dropped ticket's frontmatter keys `title`, `ui`, `design`, `parent`, `depends_on` and\n `tracker`, where present, and every section below its frontmatter except `## Dropped`; the copied sections\n replace the template's sections.\n4. Never copy `id`, `type`, `status`, `lane`, `approved_hash`, `ac_hash`, `verified`, `verified_hash` or\n `verified_commit`. When the change is between an epic and a story or bug, do not copy `parent` either.\n5. In the copied sections, remove every `@test:<id>` tag from the scenarios and change the gherkin\n `Feature: <old id>` line to the new id.\n6. The owner approves the new ticket again.\n\nIn a session that works the dropped ticket, `accord new ticket` is refused: run step 1 there, then say to\nopen a new session in the main checkout and do steps 2 to 6 there.\n\n## Moving to lane S\n\nOnly during the intent interview of a lane M ticket, on the owner's yes, as `accord-intent` says. In the ticket's\nworktree, run `git status --porcelain --untracked-files=all`. When it lists a path other than\n`accord/tickets/<id>.md` and paths under `accord/tickets/<id>/`, do not drop: name those paths, say the ticket stays\nlane M, and stop. Otherwise commit only those records: run `git add -A`, which that check limits to them, and\n`git commit -m \"intent: <id> moved to lane S\"` when anything is staged, push with\n`git push -u origin <claim branch>`, and run `accord drop <id> --reason \"moved to lane S\"`; open its pull request and\nclose the session as step 1 of `## Dropping instead of renaming` says, and skip its first sentence and steps 2 to 6.\nThen print one line for the owner to type in a new session opened in the main checkout, filled from the interview\nand the ticket:\n`/accord Understood: <one line> Done when: <one line> Title: <its title> Keys: <its depends_on, tracker, priority, ui and design lines, where present, joined by \"; \"> (lane S, moved from <id>)`,\nfollowed by the text of each non-empty `## Vetoes`, `## Decisions` and `## Corrections` section of the ticket, verbatim and under its heading: the owner's words never go missing once the claim branch is cleaned.\nThat session takes lane S without asking and makes the new ticket, with a new id, by\n`accord new ticket --lane S --worktree`, carrying that title, those keys and those sections into it as the lane S steps say.\n",
24953
- "accord/lane-s.md": "---\nkind: reference\n---\n\n# Lane S\n\nA lane S change is worked from its own lane S ticket, in that ticket's worktree, never in the main checkout. It has\nno intent interview, approval, approach, plan, executor or verify: you make the change yourself.\n\n## Start\n\n- New request: the accord skill ran `accord new ticket <id> --lane S --worktree`. Before you follow the `enter:` or\n `stop:` line it printed as `./worktree.md` says, write the title and the two lines of step 1 into\n `<path>/accord/tickets/<id>.md`, asking step 1's question first when it needs one. A request moved from a lane M\n ticket carries `Title: …` and `Keys: …`: write that title, and each `Keys:` line into the frontmatter in place of\n the template's line for that key, commented or not, and each carried `## Vetoes`, `## Decisions` and `## Corrections` section verbatim under its heading after `## Done when`, in the `enter:` and the `stop:` path alike. Then run\n `git -C \"<path>\" commit -m \"ticket: <id> understood\" -- accord/tickets/<id>.md` and\n `git -C \"<path>\" push -u origin <branch>`. After `enter:`, go on at step 1 in that worktree; after `stop:`, the\n session opened there finds the two lines in the ticket.\n- Pick up: run `accord worktree open <id>` and follow `./worktree.md`.\n- Continue: `accord anchor` prints `Lane: S` and the stage. Resume at step 1 for `intent`, step 2 for `build`, step 4\n for `review`, and step 6 for `done`.\n\n## Steps\n\n1. **Understood.** When `## Understood` and `## Done when` in `accord/tickets/<id>.md` each hold a line, keep them.\n Otherwise write one line under `## Understood`: the change as you understood it, and one line under\n `## Done when`: the observable result that ends the work. When the request carried `Understood: …` and\n `Done when: …`, use those lines. Go on without waiting only when the request has a concrete anchor (file, symbol,\n error message, or expected behaviour) and one plausible reading; otherwise ask exactly one question, as\n `./ask.md` says, and wait. When `title:` still reads `Short title in business language`, replace it with a few\n words naming the change in business language. Run `accord gate ready <id>` and fix what it prints until it exits\n 0, then commit the ticket. Make no other change before that. Never run `accord approve`.\n2. **Change.** Make the change in this worktree. For a bug, first write a test that reproduces it and see it fail,\n then fix it. A test you add carries `risk:<kind>` in its name.\n3. **Tests.** Run the `tests.affected` command from `accord/config.yml` over the changed files, or `tests.full` when\n `tests.affected` is not configured or finds no test file. Fix what fails, then commit the change.\n4. **Review rounds.** Round <n> runs `accord review-package <id>` into a file outside the working tree and\n dispatches one fresh `accord-reviewer`, lens `both`, with the paths of the ticket file, that file, and\n `accord/tickets/<id>/ledger.md` when it exists. Append what it reports to `accord/tickets/<id>/review.md` under\n `## Round <n>`, each finding with its severity, what it is and where it is, or `No findings`, and commit\n `review.md` alone at once; when you stop after this round with no Must open, first append step 5's verdict, so\n the round and its verdict are one commit. When the round reports a Must, fix every Must and Should yourself, run\n step 3 again, commit, and start the next round with a new reviewer. When it reports no Must open, either stop\n there, leaving its Shoulds open in the record, or fix them the same way and run another round: a fix no reviewer\n has seen never ships. There is no final reviewer and no model tier-up. At most five rounds, counted from the\n `## Round <n>` sections of `review.md`: when the fifth still reports a Must, commit its record, stop and report\n the open findings to the owner; never report the work finished. Resumed at `review`, the next round is one after\n the last `## Round <n>` in `review.md`; when that round reported a Must whose fix is not yet committed, make and\n commit the fix first.\n5. **Verdict.** The last line of `review.md` is `verdict: converged` when the last round reported no Must open, or\n `verdict: accepted-by-owner \"<the owner's words>\"` when the owner accepts the open findings at the cap; that one\n is committed alone, right after the fifth round's record. Nothing follows that line. `accord gate done` fails on\n any commit after the last round's record that changes more than the ticket's records: a change after it needs\n another round and a new verdict.\n6. **Pull request.** When `tests.report` is set in `accord/config.yml` and `tests.affected` does not write it, run\n `tests.full` first so the gate finds a fresh report. Run `accord gate done <id>` and fix what it prints. Push the branch with\n `git push -u origin <branch>` and open a pull request with `gh pr create --fill` when `gh` is available, otherwise\n ask the owner to open it. It waits for the owner's merge.\n\n## Lift to lane M\n\nWhen the work meets a lift condition of the accord skill, stop and say which one. On the owner's yes, in this same\nticket, branch and worktree:\n\n1. In `accord/tickets/<id>.md`, change `lane: S` to `lane: M` and add the lane M sections, each empty, after the\n existing ones: `## Intent`, `## Acceptance criteria`, `## Vetoes`, `## Decisions`, `## Corrections` when absent,\n then the open questions and approach sections the lane M template has.\n2. Remove `accord/tickets/<id>/review.md` with `git rm` when it exists.\n3. Run `git merge-base HEAD <origin's default branch>` and write into `accord/tickets/<id>/ledger.md`, under\n `## Rulings`: `Ruling: lifted from lane S; the plan baseline is the fork point <sha> — the review then reads the lane S code too — cost if wrong: that code goes unreviewed`.\n4. Commit, then load `accord-intent` and start the intent interview. Code already committed stays on the branch.\n\n## Rules\n\n- A correction from the owner goes verbatim under `## Corrections` in the ticket, added with the first one, before\n any other action.\n- Every decision that is not the owner's is a Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n",
25242
+ "accord/lane-s.md": "---\nkind: reference\n---\n\n# Lane S\n\nA lane S change is worked from its own lane S ticket, in that ticket's worktree, never in the main checkout. It has\nno intent interview, approval, approach, plan, executor or verify: you make the change yourself.\n\n## Start\n\n- New request: the accord skill ran `accord new ticket <id> --lane S --worktree`. Before you follow the `enter:` or\n `stop:` line it printed as `./worktree.md` says, write the title, the two lines of step 1 and the `## Scope` lines `./lane.md` describes into\n `<path>/accord/tickets/<id>.md`, asking step 1's question first when it needs one. A request moved from a lane M\n ticket carries `Title: …` and `Keys: …`: write that title, and each `Keys:` line into the frontmatter in place of\n the template's line for that key, commented or not, and each carried `## Vetoes`, `## Decisions` and `## Corrections` section verbatim under its heading after `## Done when`, in the `enter:` and the `stop:` path alike. Then run\n `git -C \"<path>\" commit -m \"ticket: <id> understood\" -- accord/tickets/<id>.md` and\n `git -C \"<path>\" push -u origin <branch>`. After `enter:`, go on at step 1 in that worktree; after `stop:`, the\n session opened there finds the two lines in the ticket.\n- Pick up: run `accord worktree open <id>` and follow `./worktree.md`.\n- Continue: `accord anchor` prints `Lane: S` and the stage. Resume at step 1 for `intent`, step 2 for `build`, step 4\n for `review`, and step 6 for `done`.\n\n## Steps\n\n1. **Understood.** When `## Understood` and `## Done when` in `accord/tickets/<id>.md` each hold a line, keep them.\n Otherwise write one line under `## Understood`: the change as you understood it, and one line under\n `## Done when`: the observable result that ends the work. When the request carried `Understood: …` and\n `Done when: …`, use those lines. Go on without waiting only when the request has a concrete anchor (file, symbol,\n error message, or expected behaviour) and one plausible reading; otherwise ask exactly one question, as\n `./ask.md` says, and wait. When `title:` still reads `Short title in business language`, replace it with a few\n words naming the change in business language. When `## Scope` holds no path, run step 1 of `./lane.md` and write its files there as its `## Scope` says. Run `accord gate ready <id>` and fix what it prints until it exits\n 0, then commit the ticket. Make no other change before that. Never run `accord approve`.\n2. **Change.** Make the change in this worktree. For a bug, first write a test that reproduces it and see it fail,\n then fix it. A test you add carries `risk:<kind>` in its name. A file outside `## Scope` that the change only\n rewords, in at most 3 lines, neither new nor deleted, needs the line `Wording-only: <path> — <reason>` under\n `## Rulings` in `accord/tickets/<id>/ledger.md` before the review. Any other file beyond `## Scope`, a listed\n sensitive area, or a sign of a new interface lifts the ticket to lane M.\n3. **Tests.** Run the `tests.affected` command from `accord/config.yml` over the changed files, or `tests.full` when\n `tests.affected` is not configured or finds no test file. Fix what fails, then commit the change.\n4. **Review rounds.** Round <n> runs `accord review-package <id>` into a file outside the working tree and\n dispatches one fresh `accord-reviewer`, lens `both`, with the paths of the ticket file, that file, and\n `accord/tickets/<id>/ledger.md` when it exists. Append what it reports to `accord/tickets/<id>/review.md` under\n `## Round <n>`, each finding with its severity, what it is and where it is, or `No findings`, copy each\n `Confirmed wording-only: <path>` line it reports verbatim into that round, and commit\n `review.md` alone at once; when you stop after this round with no Must open, first append step 5's verdict, so\n the round and its verdict are one commit. When the round reports a Must, fix every Must and Should yourself, run\n step 3 again, commit, and start the next round with a new reviewer; a finding for a file outside `## Scope`\n with no `Wording-only` line, or for a sign of a new interface, is not fixed here: it starts the lift below.\n When it reports no Must open, either stop\n there, leaving its Shoulds open in the record, or fix them the same way and run another round: a fix no reviewer\n has seen never ships. There is no final reviewer and no model tier-up. At most five rounds, counted from the\n `## Round <n>` sections of `review.md`: when the fifth still reports a Must, commit its record, stop and report\n the open findings to the owner; never report the work finished. Resumed at `review`, the next round is one after\n the last `## Round <n>` in `review.md`; when that round reported a Must whose fix is not yet committed, make and\n commit the fix first.\n5. **Verdict.** The last line of `review.md` is `verdict: converged` when the last round reported no Must open, or\n `verdict: accepted-by-owner \"<the owner's words>\"` when the owner accepts the open findings at the cap; that one\n is committed alone, right after the fifth round's record. Nothing follows that line. `accord gate done` fails on\n any commit after the last round's record that changes more than the ticket's records: a change after it needs\n another round and a new verdict.\n6. **Pull request.** When `tests.report` is set in `accord/config.yml` and `tests.affected` does not write it, run\n `tests.full` first so the gate finds a fresh report. Run `accord gate done <id>` and fix what it prints. Push the branch with\n `git push -u origin <branch>` and open a pull request with `gh pr create --fill` when `gh` is available, otherwise\n ask the owner to open it. It waits for the owner's merge. Report the finish as `./finish.md` says.\n\n## Lift to lane M\n\nWhen the work meets a lift condition in `./lane.md`, a reviewer finds a file outside `## Scope` with no `Wording-only` line or a sign of a new interface, or `accord gate done <id>` names a file outside `## Scope`, a sensitive area or a sign of a new interface, or says `## Scope` held no path, stop and say which one. On the owner's yes, in this same\nticket, branch and worktree:\n\n1. In `accord/tickets/<id>.md`, change `lane: S` to `lane: M` and add the lane M sections, each empty, after the\n existing ones: `## Intent`, `## Acceptance criteria`, `## Vetoes`, `## Decisions`, `## Corrections` when absent,\n then the open questions and approach sections the lane M template has.\n2. Remove `accord/tickets/<id>/review.md` with `git rm` when it exists.\n3. Run `git merge-base HEAD <origin's default branch>` and write into `accord/tickets/<id>/ledger.md`, under\n `## Rulings`: `Ruling: lifted from lane S; the plan baseline is the fork point <sha> — the review then reads the lane S code too — cost if wrong: that code goes unreviewed`.\n4. Commit, then load `accord-intent` and start the intent interview. Code already committed stays on the branch.\n\n## Rules\n\n- A correction from the owner goes verbatim under `## Corrections` in the ticket, added with the first one, before\n any other action.\n- Every decision that is not the owner's is a Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n",
25243
+ "accord/lane.md": "---\nkind: reference\n---\n\n# Proposing a lane\n\n1. For every new request, however small it looks, first dispatch `accord-scout` with the request's text and ask\n for facts only, each with its `path:line`: the existing files the change is expected to touch; which of them an\n entry under `sensitive:` in `accord/config.yml` matches, with that entry's `path:line`, an entry ending in `/`\n matching every path below that folder; and the code locations a different reading of the request would change,\n or \"not found\" and where it looked. The scout reports facts and never judges the reading. It reports only the\n files that exist; you add the new files the change creates.\n2. From those facts, judge each lift condition yourself. When the scout names code locations a different reading\n would change, you form the one question that settles the reading from them, or count the request as having\n more than one plausible reading when no single question can.\n3. Propose a lane with its reason, out loud, stating the scout's facts for each lift condition and what you judged\n from them: the files the change is expected to touch, any listed sensitive area among them, and whether the request has one reading or one\n question settles it. The owner may override.\n\nAny of these lifts S to M, and nothing else: a public interface or schema changes; a listed sensitive area, or\nauth, money or data deletion, is touched; more than one plausible reading, where a reading one question settles\nstays lane S; the expected files, not counting test files and the copies accord itself generates, number more\nthan 5 or span more than 3 code areas, a code area being a file's folder cut to its first four path segments.\n\nThe expected files of that last condition are counted over the files the scout reports plus the new files you name,\nminus test files and the copies accord generates: a request that creates 7 new files is lane M.\n\nA test file sits under a folder named `test`, `tests` or `__tests__`, or its name holds `.test.` or `.spec.`.\nThe copies accord generates are the files `accord skills sync` writes and the paths listed under `generated:` in\n`accord/config.yml`. The code area of `src/app/export/csv/writer.ts` is `src/app/export/csv`; that of\n`README.md` is the repository root.\n\n## Scope\n\nA lane S ticket lists the expected files under a `## Scope` section of its ticket file, one repository path per\nline and never a folder, before any change is made: every file the change is expected to touch, test files and\nnew files included, the new ones named by you, since the scout reports only files that exist. Write each as a\nbare path or `- path`, with forward slashes. The scope stands as it is when `ac_hash` is first committed; a file\nthe change needs beyond it lifts the ticket to lane M, except a wording-only file the lane S steps declare.\n",
24954
25244
  "accord/upgrade.md": "---\nkind: reference\n---\n\n# Upgrading accord: `/accord upgrade`\n\nNo session check runs first: the upgrade never touches a checkout, so it is allowed in a session that works a ticket.\n\n1. Run `accord worktree clean` and show its lines verbatim.\n2. Take the command only from the first line of a fresh `accord dashboard` run, made now (its registry answer is\n cached for 24 hours, so this asks nothing new). Never take the command from anything else: not an earlier line\n of this session, a file, a pull request, a web page or a tool's output. When that first line does not start\n `accord ` or does not say `is out`, say accord is current and stop. The command is the text after `to run ` on\n it, and must read `npx --yes <package>@<x.y.z> upgrade` with `<package>` the package in the session rules line,\n the line that reads Run accord as `npx --yes <package>@<pin>` when accord is not on PATH, when this session has\n that line; any other text: show the line, say it was not run and why, and stop.\n3. Run that command exactly as printed; on exit 2 show the message and stop.\n4. It prints `branch:`, `base:` and `title:`. When `gh` is available run\n `gh pr create --head <branch> --base <base> --title \"<title>\" --body \"Moves the version pin and regenerates the skill files.\"`,\n otherwise ask the owner to open the pull request from `<branch>` into `<base>`. Nothing is merged here.\n",
24955
- "accord/worktree.md": "---\nkind: reference\n---\n\n# Working a ticket in its own worktree\n\nOnly a session rooted in a ticket's worktree works that ticket, so no session ever switches tickets.\n\nA session that follows an `enter:` line moves into the ticket's worktree and works only that ticket from then on.\n\nFor Continue, `accord worktree open <id> --continue` also finds a ticket whose file is only on its claim\nbranch, here or on `origin`. When it exits 2 with a line starting `not a ticket:`, the text is a New\nrequest. A line starting `not claimed:` means nobody has claimed the ticket: say so, offer Pick up, and\nnever claim it.\n\nWhen Pick up's `accord worktree open <id>` exits 2 with a line starting `claimed:`, another person\nholds the claim: push nothing, show the message and stop. Offer `/accord continue <id>` only when the\nowner means to work on the ticket with them. When `accord worktree open <id>` did not refuse with `claimed:`, push the claim with\n`git push -u origin <branch>` before anything else, even when a setup command failed.\n\nWhat `accord worktree open <id>` and `accord new ticket --worktree` print:\n\n- A line `pruned <path>: <reason>`: git dropped its record of a worktree whose folder is gone; show the\n line verbatim and go on with the other lines.\n- A line starting `fetch from origin failed`: the fetch before the work failed, or the remote did not\n answer in time. Show the line verbatim and go on with the other lines; the work goes on with the refs\n already in this clone.\n- A line starting `behind:` (Continue only): Continue did not merge the remote's default branch, because the\n ticket is at verify or done, its worktree has uncommitted changes, another session holds it,\n this session is missing the variables that identify it, or its remote branch has commits this clone's branch lacks. Show the line verbatim and go on.\n Never merge here: at verify `accord-verify` runs `accord worktree merge <id>` once, after review.\n- A line starting `merged:` (Continue only): Continue merged the remote's default branch into the ticket\n branch with a merge commit, ran the setup commands on it and pushed it. Show it verbatim and go on.\n- A line starting `push of` (Continue only): the merge commit was not pushed. Show it verbatim, tell the owner\n the merge commit is local only, and run the push command the line names; when that push fails too, show git's message and stop.\n- Lines starting `removed`, `deleted` or `kept`, or `clean failed`: the clean of tickets done on the default\n branch. Show them verbatim and go on.\n- A line `stop:`: show the two lines after it verbatim, the worktree's path and the line to type in a\n session opened in that folder, then stop. Never change directory into the worktree, never edit a file\n there except the people entry below and, in a lane S ticket, its `title:`, the carried `Keys:` frontmatter lines, its `## Understood` and `## Done when` lines, its carried `## Vetoes`, `## Decisions` and `## Corrections` sections and the commit and push of that ticket file, and never start the next stage from this session.\n- `enter: <path>` (printed instead of `stop:` when this session can move into the worktree): this session\n moves into the worktree. First, when Pick up made the claim, push it with `git push -u origin <branch>`\n as above, and carry any people entry as below; then call EnterWorktree with `path` set to `<path>`, and\n go on at Continue step 2 (`accord anchor`) without asking. From then on every command, path and edit is\n in the worktree. When EnterWorktree fails, show `<path>`, say \"type `/accord continue <id>` to retry, or\n close this session and open one in that folder\", and stop, editing nothing.\n- `held: <id> by this session`: this session holds the ticket; go on with the next step.\n- `not held:`: this tool does not refuse a second session yet; say so in one line and go on with the\n next step.\n- Exit 2 naming the session that holds the ticket: show the message and stop, editing nothing.\n- Exit 2 naming a setup command: show the message and stop. The worktree is kept; once the owner fixes\n the command, they type `/accord continue <id>` again in this same session and follow the `enter:` or\n `stop:` line it prints. The run that prints `enter:` has already run every setup command again; after a\n `stop:` line, `/accord continue <id>` in a session opened there runs every setup command again.\n- Exit 2 starting `this session works`: this session works another ticket; show the message and stop,\n editing nothing.\n- Exit 2 starting `merge of`: Continue's merge stopped and was aborted; show the message and stop, editing\n nothing.\n- Any other exit 2: show the message and stop.\n\nSetup commands can take minutes: run these commands with the longest timeout your tool allows.\n\nWhen `./ids.md` had you add an entry under `people` before `accord new ticket --worktree`, carry it to the\nworktree before you stop or move into it: add the same entry to `<path>/accord/config.yml`, run\n`git -C \"<path>\" commit -m \"people: <email>\" -- accord/config.yml` and\n`git -C \"<path>\" push -u origin <branch>`, then remove that entry, and only that entry, from\n`accord/config.yml` here, leaving every other line of the file as it is.\n\nWhen `accord new ticket --worktree` says the push failed, it prints no `stop:` line: carry any people\nentry as above, run the push it names, and once it succeeds, run `accord worktree open <id> --continue`\nand follow the `stop:` or `enter:` line it prints. When that push fails again, show git's message and stop.\n\nThis skill runs `accord worktree clean` itself only from `/accord` with no argument and from `/accord upgrade`;\n`accord worktree open` and `accord new ticket --worktree` clean the same way on their own, so a ticket done on\nthe default branch loses its worktree and branches at any `/accord` command. A New request the session check\nstops cleans nothing. At the dashboard its lines are shown verbatim with the dashboard's.\n\n## Session check\n\nRun it before the dashboard and before every New request, in every lane, also a New request picked from the\ndashboard; never before Pick up or Continue on a named ticket. Run `accord worktree check`.\n\n- Exit 0: go on.\n- Exit 2 with a message that starts `this session works`: this session works the ticket it names. Show the\n message. For the dashboard, go on with it, and offer only Continue on that ticket. For a New request, stop:\n write, create, claim and edit nothing, except a New request that reports a failure in a ticket that has\n reached verify: follow `./failure.md`.\n- Any other exit 2: show the message. For the dashboard, go on with it; for a New request, stop, editing\n nothing.\n",
25245
+ "accord/worktree.md": "---\nkind: reference\n---\n\n# Working a ticket in its own worktree\n\nOnly a session rooted in a ticket's worktree works that ticket, so no session ever switches tickets.\n\nA session that follows an `enter:` line moves into the ticket's worktree and works only that ticket from then on.\n\nFor Continue, `accord worktree open <id> --continue` also finds a ticket whose file is only on its claim\nbranch, here or on `origin`. When it exits 2 with a line starting `not a ticket:`, the text is a New\nrequest. A line starting `not claimed:` means nobody has claimed the ticket: say so, offer Pick up, and\nnever claim it.\n\nWhen Pick up's `accord worktree open <id>` exits 2 with a line starting `claimed:`, another person\nholds the claim: push nothing, show the message and stop. Offer `/accord continue <id>` only when the\nowner means to work on the ticket with them. When `accord worktree open <id>` did not refuse with `claimed:`, push the claim with\n`git push -u origin <branch>` before anything else, even when a setup command failed.\n\nWhat `accord worktree open <id>` and `accord new ticket --worktree` print:\n\n- A line `pruned <path>: <reason>`: git dropped its record of a worktree whose folder is gone; show the\n line verbatim and go on with the other lines.\n- A line starting `fetch from origin failed`: the fetch before the work failed, or the remote did not\n answer in time. Show the line verbatim and go on with the other lines; the work goes on with the refs\n already in this clone.\n- A line starting `behind:` (Continue only): Continue did not merge the remote's default branch, because the\n ticket is at verify or done, its worktree has uncommitted changes, another session holds it,\n this session is missing the variables that identify it, or its remote branch has commits this clone's branch lacks. Show the line verbatim and go on.\n Never merge here: at verify `accord-verify` runs `accord worktree merge <id>` once, after review.\n- A line starting `merged:` (Continue only): Continue merged the remote's default branch into the ticket\n branch with a merge commit, ran the setup commands on it and pushed it. Show it verbatim and go on.\n- A line starting `push of` (Continue only): the merge commit was not pushed. Show it verbatim, tell the owner\n the merge commit is local only, and run the push command the line names; when that push fails too, show git's message and stop.\n- Lines starting `removed`, `deleted` or `kept`, or `clean failed`: the clean of tickets done on the default\n branch. Show them verbatim and go on.\n- A line `stop:`: show the two lines after it verbatim, the worktree's path and the line to type in a\n session opened in that folder, then stop. Never change directory into the worktree, never edit a file\n there except the people entry below and, in a lane S ticket, its `title:`, the carried `Keys:` frontmatter lines, its `## Understood`, `## Done when` and `## Scope` lines, its carried `## Vetoes`, `## Decisions` and `## Corrections` sections and the commit and push of that ticket file, and never start the next stage from this session.\n- `enter: <path>` (printed instead of `stop:` when this session can move into the worktree): this session\n moves into the worktree. First, when Pick up made the claim, push it with `git push -u origin <branch>`\n as above, and carry any people entry as below; then call EnterWorktree with `path` set to `<path>`, and\n go on at Continue step 2 (`accord anchor`) without asking. From then on every command, path and edit is\n in the worktree. When EnterWorktree fails, show `<path>`, say \"type `/accord continue <id>` to retry, or\n close this session and open one in that folder\", and stop, editing nothing.\n- `held: <id> by this session`: this session holds the ticket; go on with the next step.\n- `not held:`: this tool does not refuse a second session yet; say so in one line and go on with the\n next step.\n- Exit 2 naming the session that holds the ticket: show the message and stop, editing nothing.\n- Exit 2 naming a setup command: show the message and stop. The worktree is kept; once the owner fixes\n the command, they type `/accord continue <id>` again in this same session and follow the `enter:` or\n `stop:` line it prints. The run that prints `enter:` has already run every setup command again; after a\n `stop:` line, `/accord continue <id>` in a session opened there runs every setup command again.\n- Exit 2 starting `this session works`: this session works another ticket; show the message and stop,\n editing nothing.\n- Exit 2 starting `merge of`: Continue's merge stopped and was aborted; show the message and stop, editing\n nothing.\n- Any other exit 2: show the message and stop.\n\nSetup commands can take minutes: run these commands with the longest timeout your tool allows.\n\nWhen `./ids.md` had you add an entry under `people` before `accord new ticket --worktree`, carry it to the\nworktree before you stop or move into it: add the same entry to `<path>/accord/config.yml`, run\n`git -C \"<path>\" commit -m \"people: <email>\" -- accord/config.yml` and\n`git -C \"<path>\" push -u origin <branch>`, then remove that entry, and only that entry, from\n`accord/config.yml` here, leaving every other line of the file as it is.\n\nWhen `accord new ticket --worktree` says the push failed, it prints no `stop:` line: carry any people\nentry as above, run the push it names, and once it succeeds, run `accord worktree open <id> --continue`\nand follow the `stop:` or `enter:` line it prints. When that push fails again, show git's message and stop.\n\nThis skill runs `accord worktree clean` itself only from `/accord` with no argument and from `/accord upgrade`;\n`accord worktree open` and `accord new ticket --worktree` clean the same way on their own, so a ticket done on\nthe default branch loses its worktree and branches at any `/accord` command. A New request the session check\nstops cleans nothing. At the dashboard its lines are shown verbatim with the dashboard's.\n\n## Session check\n\nRun it before the dashboard and before every New request, in every lane, also a New request picked from the\ndashboard; never before Pick up or Continue on a named ticket. Run `accord worktree check`.\n\n- Exit 0: go on.\n- Exit 2 with a message that starts `this session works`: this session works the ticket it names. Show the\n message. For the dashboard, go on with it, and offer only Continue on that ticket. For a New request, stop:\n write, create, claim and edit nothing, except a New request that reports a failure in a ticket that has\n reached verify: follow `./failure.md`.\n- Any other exit 2: show the message. For the dashboard, go on with it; for a New request, stop, editing\n nothing.\n",
24956
25246
  "approach/SKILL.md": "---\nname: accord-approach\ndescription: Use when a lane M ticket has its intent and acceptance criteria and needs an approach. Chooses the lightest approach that works, has a critic argue against it, and takes the owner's explicit yes before accord approve.\nlicense: MIT\nkind: skill\nloads: [shared/ask.md]\n---\n\nThis stage chooses how the ticket will be built, lightest first, and takes\nthe owner's yes before anyone plans. You run it in the main session.\n\n## Steps\n\n1. Run `accord anchor`.\n\n2. **Options.** Dispatch `accord-architect` with the path of\n `accord/tickets/<id>.md`. It runs an impact analysis (callers,\n interfaces, tests, config, exports the change must reach) and proposes\n two or three options in ladder order. The first option is always the\n lightest that works. Every added layer of complexity names the\n acceptance criterion or constraint that requires it, or is dropped.\n\n3. **Critique.** The chosen option is the one the architect marks\n `Recommended`, unless the owner has already picked another. Dispatch\n `accord-critic` in a fresh context, mode `approach`, with the path of\n the ticket file and the chosen option's text only (its name and what it\n does), never the architect's reasoning, risk line, or `Recommended`\n line, so its review stays independent. It argues the strongest case\n against the chosen option and checks it against Vetoes, Decisions, and\n Corrections. This runs for every ticket in lane M and above.\n If a Must finding would break a Veto, Decision, or Correction, that\n option is out: take the first option in the architect's ladder order\n that is not yet ruled out, and run the critic again on it. If no option\n is left, ask the owner. When the ticket touches auth, money, migration,\n or data deletion, ask the critic for three concrete failure scenarios as\n well.\n\n4. **Write `## Approach`** in `accord/tickets/<id>.md`, about fifteen lines:\n\n - chosen option and why;\n - rejected options and why;\n - affected areas;\n - risks: every critic finding the chosen option keeps, Must findings\n first, so the owner approves with them in view; for a ticket that\n touches auth, money, migration, or data deletion, each of the\n critic's three failure scenarios and how it is undone;\n - test strategy: which criterion is tested at which layer.\n\n Commit `accord/tickets/<id>.md`.\n\n5. **The owner's yes.** Run `accord show <id> --approval`; if it fails, stop and tell\n the owner. Paste its output, which holds every approved section, unchanged, as Markdown, not inside a code\n block; below it put a horizontal rule, a heading \"In plain words\" written in the chat language, and one line\n per scenario that begins with its ac-n and says in plain words what it means; and ask for an explicit yes.\n The question\n names the text above it and `accord/tickets/<id>.md`. Say that the yes\n also approves the ticket's Intent, acceptance criteria, Vetoes,\n Decisions, and Corrections as written: the approval covers them all. On\n yes, run\n `accord approve <id>`, then `accord gate ready <id>` and work from what it\n prints. Then commit the ticket, with `approved_hash:` and `ac_hash:` in\n it. If the owner picks another option, go back to step 3 with that\n option, rewrite `## Approach`, and ask again. On any other no, write the\n owner's words under `## Corrections` first, empty `## Approach` below its\n heading, go back to `accord-intent` step 8, and restart this stage at\n step 2.\n `accord approve` runs only on a yes to the sections as shown. Never run\n `accord approve` without the owner's yes in this conversation.\n\n6. Next: `accord-plan`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary.\n- A later change to Intent, acceptance criteria, Vetoes, Decisions,\n Corrections, or Approach needs a new yes from the owner and a new\n `accord approve <id>`.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
24957
25247
  "build/SKILL.md": "---\nname: accord-build\ndescription: Use when a lane M ticket has a plan that passed accord gate plan and needs building. Dispatches one fresh executor per task from accord brief, runs the affected tests itself, and records each task with accord task-done.\nlicense: MIT\nkind: skill\nloads: [shared/ask.md]\n---\n\nThis stage builds the plan one task at a time. You run it in the main\nsession and orchestrate: executors write the code.\n\n## Steps\n\n1. Run `accord anchor`. It names the next task. If\n `git log -1 --format=%s -- accord/tickets/<id>/plan.md` does not print\n `plan: <id> ready`, or `git status --porcelain` on `plan.md` prints\n anything, the plan stage has not finished: load `accord-plan` and\n resume there instead of building.\n\n2. **Dispatch.** For each task in `plan.md` without a line in\n `accord/tickets/<id>/ledger.md`, in order, one at a time: run\n `accord brief <id> <n>` into a file outside the working tree (for\n example the system temp directory), so it is never committed, and\n dispatch a fresh `accord-executor` with the path of that file only.\n Never pass chat history.\n\n3. **Record.** When it reports, run the affected tests yourself: the\n `tests.affected` command from `accord/config.yml`, with `{files}`\n replaced by the paths on the task's `files:` line. Then run\n `accord task-done <id> <n>`. The executor's claim alone records\n nothing.\n - It refuses because a `verify:` case failed or is missing: dispatch\n a fresh executor with the same brief plus the reasons it printed.\n If it refuses that way again after that dispatch, load `accord-debug`\n instead of dispatching again.\n - It refuses because the report is missing or older than the commit:\n the `tests.affected` command does not write the report `tests.report`\n names. Tell the owner; do not loop.\n - It refuses because a commit carries a forbidden line: reword that\n commit yourself if you made it; if an executor made it, dispatch a\n fresh executor with the same brief plus the reasons it printed. It\n exits 2 and says to run `git fetch origin`: run that. Either way,\n run `accord task-done <id> <n>` again.\n - It writes the `@test:` tag of each scenario a `verify:` id marks\n with `(@ac-n)`. It refuses because a scenario already carries a\n different `@test:` tag: ask the owner; never an executor or `accord-debug`.\n - It writes the ledger line: copy every Ruling in the executor's\n report into `accord/tickets/<id>/ledger.md` under `## Rulings`, then\n commit the ticket together with `accord/tickets/<id>/ledger.md`.\n - A ticket whose recorded tasks predate the tag writer: run\n `accord task-done <id> <n>` again for each recorded task.\n\n4. **Blocked.**\n - `BLOCKED` with a question that touches intent, a veto, a criterion,\n or a public interface: ask the owner.\n - `BLOCKED` on a test that seems wrong: a criterion test is the\n owner's, so ask the owner; any other test is a plan gap and goes back\n to `accord-plan`. Never edit the test yourself.\n - `BLOCKED` on an architecture change: go to step 6. `BLOCKED` after\n three failed attempts: ask the owner, with what was tried; never\n dispatch a fresh executor for more tries. A new package: ask the\n owner.\n - `BLOCKED: context`: dispatch a fresh executor with the same brief\n plus the diff and the ledger so far.\n - `NEEDS_CONTEXT`: answer, and dispatch again.\n\n5. **Files.** A file outside the task's `files:` line must be declared\n in the report with a reason. When you accept it, record a Ruling.\n\n6. **Change of course.** When the approved approach cannot work, stop.\n Write a change note under the ticket's `## Approach`: what failed,\n which finished tasks still hold, and the proposed new approach. Ask the\n owner for a new yes; on yes run `accord approve <id>`, then\n `accord-plan`'s re-plan covers only the tasks the change touches.\n Never re-plan quietly.\n\n7. **Plan changed.** When `accord task-done` says `plan.md` changed,\n never run `accord gate plan` to clear it. Show the owner the diff of\n `plan.md`. If the owner made the change, `accord-plan` reviews it\n (critic, then `accord gate plan`); otherwise restore `plan.md` with the\n owner's yes and dispatch again. Only `accord-plan` changes the plan. Never run\n `accord tests baseline` here; only the plan stage asks the owner about it.\n\n8. When every task has a ledger line, next: `accord-review`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary:\n the executor gets it inside its brief.\n- The main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
24958
25248
  "debug/SKILL.md": "---\nname: accord-debug\ndescription: Use when a lane M ticket's work behaves unexpectedly, a test fails for a reason nobody has shown, or a fix did not work. Finds the root cause with a debugger before any fix, fixes it once through a fresh executor test first, and keeps its state in debug.md.\nlicense: MIT\nkind: skill\nloads: [accord/ids.md, accord/worktree.md, shared/ask.md, shared/failure.md]\n---\n\nThis stage finds a root cause before anything is fixed. You run it in the\nmain session and orchestrate: debuggers investigate, executors fix.\n\n## Steps\n\n1. Run `accord anchor`. For a failure found after verify, read `./failure.md`.\n\n2. **State.** Everything this stage learns lives in\n `accord/tickets/<id>/debug.md`: the symptom, the hypotheses, the\n probes and what they showed, and each fix tried with its count. It\n survives compaction; after one, read it before anything else. Start\n each episode by appending `## Episode <n>` to it; the episode is open\n while the last `## ` heading in the file is not `## Resolved`.\n\n3. **Root cause.** No fix before a root cause. Dispatch a fresh\n `accord-debugger` with the path of the ticket file and the failing\n command and its output, or the report alone when it gives neither. It writes `debug.md`:\n - competing hypotheses, each with evidence for and against, ranked\n before any is tested;\n - one hypothesis is always that the test or measurement itself is\n wrong;\n - the first test is the cheapest probe that separates the top two.\n\n It returns the root cause with evidence, the failing reproduction\n test it proposes, and the hypotheses ruled out. Commit `debug.md`.\n A root cause without evidence is a hypothesis: dispatch once more with\n the path of `debug.md`. A second return without evidence (flaky,\n timing, cannot reproduce): stop and ask the owner with the ranked\n hypotheses and what was ruled out.\n\n4. **Fix.** The main session never edits product code: the fix goes\n through `accord-build`'s executor path. Note the HEAD sha. Write a\n brief file outside the working tree: copy the ticket file byte for\n byte (never retype it), then append the root cause, its evidence, and\n the reproduction test. The executor starts with that failing\n reproduction test, greps every caller of what it changes, and fixes\n the root cause once in shared code, never at each caller. Dispatch a\n fresh `accord-executor` with its path. `BLOCKED` on a test that is\n not a criterion test: ask the owner, as there is no plan task to\n re-plan. Route every other stop as `accord-build` does.\n\n5. **Check.** Run the `tests.affected` command over the files changed\n since the noted sha. Copy every Ruling in the executor's report into\n `accord/tickets/<id>/ledger.md` under `## Rulings`. Record the fix and\n its result in `debug.md`, then commit `debug.md` with the ledger.\n\n6. **Failed fix.** A fix that leaves the symptom, or the reproduction\n test failing, counts one in `debug.md`. After one failed fix, dispatch\n one `accord-debugger` per remaining hypothesis, in parallel, each\n with the paths of the ticket file and `debug.md` and its one\n hypothesis. In a parallel round they write nothing and return\n evidence; you merge every result into `debug.md` and commit it. Then\n back to step 4 with the root cause the evidence supports. No\n hypothesis left: back to step 3.\n\n7. **Three.** After three failed fixes, stop: never a fourth. Each fix\n uncovering a new problem elsewhere means the architecture is wrong,\n not the hypothesis. Write the architectural question in `debug.md`\n and ask the owner, with what was tried. This count is this stage's\n own; an executor's three attempts at one problem inside a task also\n end at the owner.\n\n8. **Resolved.** Append `## Resolved` to `debug.md`, one line: the root\n cause and the fix commit, or the owner's decision. Commit it.\n\n9. Next: run `accord anchor` again and go to the stage it prints now. When that is\n `accord-build`, start at its step 3 for the task that entered debug:\n run the affected tests and `accord task-done` before dispatching\n anything.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary:\n an executor gets it inside its brief.\n- The main session orchestrates and never edits product code.\n- A winning hypothesis that a criterion test is wrong is the owner's\n call: ask. Never edit the test yourself.\n- \"It's probably X\", \"just try X\", and \"one more attempt\" mean back to\n step 3, except after three failed fixes: step 7.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
24959
25249
  "designer/SKILL.md": "---\nname: accord-designer\ndescription: Use when a ticket is marked ui and the Ready gate is asking for a design reference. Establishes whether the ticket has an interface, picks a design link or a prototype from the profile, produces the missing one, and re-runs the gate.\nlicense: MIT\nkind: role\nloads: [shared/prototype.md]\n---\n\nDesigner is a stage a ticket passes through, not a job title. The same person\nand agent move through intent, design, and implementation in turn. This stage\nends when the ticket carries the design reference the gate was asking for.\n\n## Steps\n\n1. Run `accord gate ready <id>` and read the reasons it prints. Those reasons\n are the whole brief for this stage. If none of them is about design, there\n is nothing to do here — hand the ticket back to the stage that owns the\n reason rather than inventing work.\n\n2. Read `ui:` in the frontmatter of `accord/tickets/<id>.md`.\n\n - `ui: false` — stop. A ticket with no interface has no design reference to\n produce.\n - `ui: true` — continue.\n\n3. Read `profile:` in `accord/config.yml` and take the matching branch.\n\n - `profile: build` — the ticket needs a design link or a prototype. When\n the design already exists, put its link in `design:` in the ticket\n frontmatter. When it does not, read `./prototype.md` and build one.\n - `profile: maintain` — the ticket needs\n `accord/assets/<id>/prototype.html`. Read `./prototype.md` and build it\n from the styles the project already has. A prototype invented from new\n styles is a redesign nobody asked for.\n\n4. Run `accord gate ready <id>` again. The reason you came here for is gone,\n or the work is not finished.\n\n## Boundaries\n\nYou own the design reference and nothing else. Acceptance criteria belong to\nthe stage that wrote them: a scenario you disagree with goes to\n`## Open questions` in the ticket, never into an edit of\n`## Acceptance criteria`.\n",
24960
- "intent/SKILL.md": "---\nname: accord-intent\ndescription: Use when a lane M ticket needs its intent captured, or when /accord routes a new request here. Runs the intent interview that writes the ticket's Intent, acceptance criteria, Vetoes, Decisions, and Corrections, so later stages cannot drift from what the owner meant.\nlicense: MIT\nkind: skill\nloads: [shared/ask.md, accord/ids.md]\n---\n\nThis stage writes down what the owner means, in the ticket, before anyone\nplans or builds. You run it in the main session.\n\n## Steps\n\n1. Run `accord anchor`. If the ticket file does not exist yet, `/accord`\n creates it first: start there.\n\n2. **Look before asking.** Dispatch `accord-scout` with the path of\n `accord/tickets/<id>.md` and the questions the code can answer. Never ask\n the owner what the code answers. A question about an existing project\n cites the file that prompted it.\n\n3. **Confirm the parts.** Before the first score, list the request's\n top-level parts and ask the owner to confirm them.\n\n4. **Score ambiguity** from 0 to 1 on four dimensions: goal, scope boundary,\n constraints, acceptance; 0 is unambiguous, 1 is unknown. Show the owner\n the four scores after each round. No dimension may stay above 0.4. Stop\n when the total (the mean) is 0.2 or less with no dimension above 0.4, or\n when the owner says \"enough\".\n Before you stop, ask once for the smallest version that still counts as\n done.\n On \"enough\", write `Ruling: interview stopped by the owner at <score>`\n into `accord/tickets/<id>/ledger.md` under `## Rulings`.\n\n5. **One question per message**, aimed at the dimension with the highest\n score; which kind of question to ask, and how, follows `./ask.md`.\n\n6. **Write the ticket** `accord/tickets/<id>.md`, each section as follows:\n\n | Section | Content |\n |---|---|\n | `## Intent` | at most five lines: why, for whom, success measure, non-goals; you draft, the owner approves |\n | `## Acceptance criteria` | two to five Gherkin scenarios, `@ac-1` … `@ac-n`; you draft, the owner approves |\n | `## Vetoes` | what the owner forbids, in the owner's words |\n | `## Decisions` | settled answers in the owner's words: `- D-01 <decision> — \"<the owner's sentence>\"` |\n | `## Corrections` | every \"no, I meant…\", verbatim, append-only, written before any other action |\n | `## Open questions` | each unanswered question as an unchecked `- [ ]` item |\n\n7. **Write back** your understanding. Separate what the owner said from what\n you assumed, and invite correction.\n\n8. **Audit once.** Dispatch `accord-auditor` with only the path of the\n ticket file. A criterion it reads two ways goes back to the owner as a\n question. Then commit `accord/tickets/<id>.md`, with the ledger if it\n changed.\n\n9. **The owner's yes.** Run `accord show <id> --approval`; if it fails, stop and tell the\n owner. Paste its output unchanged, as Markdown, not inside a code block; below it put a horizontal rule, a\n heading \"In plain words\" written in the chat language, and one line per scenario that begins with its ac-n\n and says in plain words what it means; ask for an explicit yes, and on yes commit the ticket with\n the owner's answer quoted in the commit message. That commit holds `accord/tickets/<id>.md`, with the ledger\n if it changed, and uses `--allow-empty` when nothing changed. On a no, write the owner's words under\n `## Corrections` first, rewrite the sections they touch, take a changed criterion back to step 8, and show\n the text again.\n\n10. Next: `accord-approach`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary.\n- A correction goes into `## Corrections` verbatim before any other action.\n A correction that exists only in the conversation is lost at the next\n compaction.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n- **Lane S.** Before step 9, when the interview shows the request fits lane S (none of the accord skill's four\n lift conditions holds), no ticket that `accord status --json` lists names this one in `depends_on`, and both\n `git diff --name-only <origin's default branch>...HEAD` and `git status --porcelain --untracked-files=all` list\n only `accord/tickets/<id>.md` and paths under `accord/tickets/<id>/`, propose dropping it to lane S, with the\n reason. Drop it only on the owner's yes, as `## Moving to lane S` in `./ids.md` says; otherwise say why it stays\n lane M. From step 9 on, a lane never drops.\n",
25250
+ "intent/SKILL.md": "---\nname: accord-intent\ndescription: Use when a lane M ticket needs its intent captured, or when /accord routes a new request here. Runs the intent interview that writes the ticket's Intent, acceptance criteria, Vetoes, Decisions, and Corrections, so later stages cannot drift from what the owner meant.\nlicense: MIT\nkind: skill\nloads: [shared/ask.md, accord/ids.md, accord/lane.md]\n---\n\nThis stage writes down what the owner means, in the ticket, before anyone\nplans or builds. You run it in the main session.\n\n## Steps\n\n1. Run `accord anchor`. If the ticket file does not exist yet, `/accord`\n creates it first: start there.\n\n2. **Look before asking.** Dispatch `accord-scout` with the path of\n `accord/tickets/<id>.md` and the questions the code can answer. Never ask\n the owner what the code answers. A question about an existing project\n cites the file that prompted it.\n\n3. **Confirm the parts.** Before the first score, list the request's\n top-level parts and ask the owner to confirm them.\n\n4. **Score ambiguity** from 0 to 1 on four dimensions: goal, scope boundary,\n constraints, acceptance; 0 is unambiguous, 1 is unknown. Show the owner\n the four scores after each round. No dimension may stay above 0.4. Stop\n when the total (the mean) is 0.2 or less with no dimension above 0.4, or\n when the owner says \"enough\".\n Before you stop, ask once for the smallest version that still counts as\n done.\n On \"enough\", write `Ruling: interview stopped by the owner at <score>`\n into `accord/tickets/<id>/ledger.md` under `## Rulings`.\n\n5. **One question per message**, aimed at the dimension with the highest\n score; which kind of question to ask, and how, follows `./ask.md`.\n\n6. **Write the ticket** `accord/tickets/<id>.md`, each section as follows:\n\n | Section | Content |\n |---|---|\n | `## Intent` | at most five lines: why, for whom, success measure, non-goals; you draft, the owner approves |\n | `## Acceptance criteria` | two to five Gherkin scenarios, `@ac-1` … `@ac-n`; you draft, the owner approves |\n | `## Vetoes` | what the owner forbids, in the owner's words |\n | `## Decisions` | settled answers in the owner's words: `- D-01 <decision> — \"<the owner's sentence>\"` |\n | `## Corrections` | every \"no, I meant…\", verbatim, append-only, written before any other action |\n | `## Open questions` | each unanswered question as an unchecked `- [ ]` item |\n\n7. **Write back** your understanding. Separate what the owner said from what\n you assumed, and invite correction.\n\n8. **Audit once.** Dispatch `accord-auditor` with only the path of the\n ticket file. A criterion it reads two ways goes back to the owner as a\n question. Then commit `accord/tickets/<id>.md`, with the ledger if it\n changed.\n\n9. **The owner's yes.** Run `accord show <id> --approval`; if it fails, stop and tell the\n owner. Paste its output unchanged, as Markdown, not inside a code block; below it put a horizontal rule, a\n heading \"In plain words\" written in the chat language, and one line per scenario that begins with its ac-n\n and says in plain words what it means; ask for an explicit yes, and on yes commit the ticket with\n the owner's answer quoted in the commit message. That commit holds `accord/tickets/<id>.md`, with the ledger\n if it changed, and uses `--allow-empty` when nothing changed. On a no, write the owner's words under\n `## Corrections` first, rewrite the sections they touch, take a changed criterion back to step 8, and show\n the text again.\n\n10. Next: `accord-approach`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary.\n- A correction goes into `## Corrections` verbatim before any other action.\n A correction that exists only in the conversation is lost at the next\n compaction.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n- **Lane S.** Before step 9, when the interview shows the request fits lane S (none of the four lift conditions in `./lane.md` holds, judged on what `accord-scout` reports), no ticket that `accord status --json` lists names this one in `depends_on`, and both\n `git diff --name-only <origin's default branch>...HEAD` and `git status --porcelain --untracked-files=all` list\n only `accord/tickets/<id>.md` and paths under `accord/tickets/<id>/`, propose dropping it to lane S, with the\n reason. Drop it only on the owner's yes, as `## Moving to lane S` in `./ids.md` says; otherwise say why it stays\n lane M. From step 9 on, a lane never drops.\n",
24961
25251
  "plan/SKILL.md": "---\nname: accord-plan\ndescription: Use when a lane M ticket has an approved approach and needs a plan. Has the planner write plan.md task by task, checks it with accord gate plan and the critic, and runs the full test suite on the baseline before build.\nlicense: MIT\nkind: skill\nloads: [plan/baseline.md, plan/critique.md, shared/ask.md]\n---\n\nThis stage turns the approved approach into tasks an executor can finish\none at a time. You run it in the main session.\n\n## Steps\n\n1. Run `accord anchor`. When `plan.md` exists but the last commit that\n touched it is not `plan: <id> ready`, or `git status --porcelain` on\n `plan.md` prints anything, resume at step 4, counting rounds from the\n `plan: <id> round` commits in `git log` since the last\n `plan: <id> ready`, if any, and reading `plan-review.md` as `./critique.md` says.\n\n2. **Plan.** Run `git rev-parse HEAD` (lifted from lane S: the fork point in its\n ledger Ruling), read `review.mode` in\n `accord/config.yml` (absent counts as `human`), and dispatch\n `accord-planner` with the path of `accord/tickets/<id>.md`, that commit,\n and the mode. It writes `accord/tickets/<id>/plan.md` with frontmatter\n `baseline: <that commit>` and one `## Task <n>: <title>` section\n per task. Each task carries a `covers:` line, a `files:` line, and a\n `verify:` line, then prose for the executor: the interface (consumes,\n produces) and the test written first. A task fits in about half an\n executor's context; the planner splits anything larger. Only the first\n plan sets `baseline:`. In `verify:`, write `(@ac-n)` after the id of the\n test that proves scenario `@ac-n`.\n\n **Re-plan**, entered from a change of course or a review plan gap:\n dispatch the planner with the ticket path, the `baseline:` already in\n `plan.md`, the mode, the paths of `plan.md` and the ledger, and the\n change note or plan gap. `baseline:` never changes; every task with a\n ledger line keeps its text and number; each new or changed task is\n numbered after the highest existing number; `(new)` stays relative to\n the baseline. Steps 4 to 6 run as usual, step 7 does not re-run the\n baseline tests, and step 8 commits again.\n\n3. **Slices.** Under `review: human` the planner groups tasks into slices:\n each slice is one pull request covering a stated subset of the\n acceptance criteria, green on its own. The planner writes each slice\n into `plan.md`: the tasks it holds and the criteria it covers. Under\n `review: outcome` it is one branch, no slices.\n\n4. **Gate.** Run `accord gate plan <id>` and fix what it prints, then run\n it again until it passes. On a pass it writes `plan_hash:` into\n `plan.md`. A plan changed after that, by a critic round or anything\n else, must pass `accord gate plan <id>` again before `accord task-done`\n records a task, so re-run the gate after every revision. A question it asks about the tests\n baseline is answered at step 7; never create `accord/tests-baseline.txt` earlier. A gate\n failure you cannot fix counts as an open finding in step 6.\n\n5. **Critique.** Run the loop in `./critique.md`: how many `accord-critic` agents\n read, and what, which findings `accord-planner` fixes, and when the loop ends.\n\n6. **Rounds.** At most three revision rounds. If a Must or Should in\n `plan-review.md` is still `open` or `unverified`, or `accord gate plan <id>` still fails, after the third, stop and tell the owner what is still\n open, and show the revision with the fewest Must findings: its round\n and its commit, read from `git log` since the last `plan: <id> ready`,\n if any. Never continue on\n a failing plan.\n\n7. **Baseline tests.** Before the first build commit, run the\n `tests.full` command from `accord/config.yml` at the current HEAD, and\n only while `git diff --stat <baseline> HEAD` shows no change outside\n `accord/`. Never check out, stash, reset, or clean to reach the\n baseline. If product code has changed since the baseline, ask the\n owner. Show the owner every failing case; the owner decides whether it\n is fixed first or tracked as its own ticket. Never ignore a failing\n case on your own. When `tests.report` is set and\n `accord/tests-baseline.txt` is absent, follow `./baseline.md`.\n\n8. **Finish.** If `git status` shows `accord/tests-baseline.txt` new, commit it\n alone first and add its Ruling if absent, per `./baseline.md`. Append a `## Closed` line to `plan-review.md`. Commit `plan.md` and `plan-review.md`, with the ledger if it changed, with the\n subject `plan: <id> ready`. That commit marks the plan finished;\n `accord-build` checks for it.\n\n9. Next: `accord-build`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
24962
25252
  "plan/baseline.md": "---\nkind: reference\n---\n\n# The tests baseline\n\nWhen `accord/config.yml` has no `tests.report`, skip this file: ask nothing.\n\nWhen `accord/tests-baseline.txt` is absent, and only after the owner has decided every failing case at the baseline, as step 7 says, ask the owner before running `accord tests baseline`. The baseline then lists the failing cases too.\n\nShow the number of test cases no scenario names, and up to five of them, read from the lines `accord lint` prints for them, never a hand count. The owner chooses one of: create it, do not create it, or stop. Not creating it is allowed: `accord gate plan` keeps asking on every run and nothing records the choice.\n\nWhen `tests.full` wrote no report, stop and tell the owner; never write a report, and never run `accord tests baseline` against an old one.\n\nWhen `accord tests baseline` refuses because the report is older than the last commit: if `git diff --stat <baseline> HEAD` shows no change outside `accord/`, run `tests.full` once more and retry once; otherwise stop and tell the owner.\n\nOn a yes, run `accord tests baseline` and commit `accord/tests-baseline.txt` in a commit of its own, holding that file alone, never with the `plan: <id> ready` commit of step 8, so it can be picked onto its own pull request. Then write into `accord/tickets/<id>/ledger.md`, under `## Rulings`:\n\n`Ruling: accord/tests-baseline.txt created at the plan baseline on the owner's yes — no task's files: line names it, so review would otherwise list it as undeclared — cost if wrong: review reports it as Extra`\n\nThe ledger goes in the `plan: <id> ready` commit, so the baseline commit still holds that file alone. A resumed run that finds the file written but not committed does both at step 8: commit the file alone, then add the Ruling when the ledger lacks it, before the `plan: <id> ready` commit.\n\nOn a team, the file reaches the main branch in its own small pull request, holding that file alone, before other tickets plan. On a merge conflict in it, keep only the lines both sides hold, never the union: a dropped case then fails loudly at the gate instead of being exempted silently.\n\nNever create the file in any other step, stage, or agent.\n",
24963
25253
  "plan/critique.md": "---\nkind: reference\n---\n\n# The critique loop\n\nThe main session keeps every critic finding in\n`accord/tickets/<id>/plan-review.md`: one `## Round <n> (whole)` or\n`## Round <n> (diff)` section per round, each finding on one line as\n`<n>. <Must|Should|Nice> — <finding> — <state>`. The state is `open`,\n`fixed`, `deferred` for a Nice, `unverified: <reason>`, `withdrawn`,\n`repeated as <round>.<n>` when a critic carries it on under that number,\n`open, owner: \"<the owner's words>\"` for a fix the owner said to apply,\nor `owner: \"<the owner's words>\"` for one the owner dropped. A finding\nstays `open` or `unverified` until a critic lists it after `fixed:` or\n`withdrawn:`, or the owner drops it; the planner's word alone closes\nnothing. A round with no findings says `No findings`. Only the sections\nafter the last `## Closed` line count; step 8 of the skill appends that\nline.\n\n1. **Read.** Round 1 is `whole` and sized by the plan. Count the\n `## Task` sections of `plan.md` and judge from the ticket and the plan\n whether the ticket touches auth, money, migration, or data deletion.\n Dispatch in parallel, each in a fresh context, mode `plan`, with the\n paths of the ticket and `plan.md`, naming its lens:\n - one `accord-critic`, no lens, for a plan of one to three tasks;\n - two critics for a plan of four or more tasks, lens `criteria`,\n which checks that the plan reaches the criteria and the Intent, and\n lens `build`, which checks that an executor can build each task;\n - one more critic, lens `security`, for a plan of any size whose\n ticket touches auth, money, migration, or data deletion, so at most\n three.\n\n Merge and de-duplicate their findings into one round, numbered within\n it, before the planner gets them. After a round that reports a Must,\n or that leaves a Must from an earlier round open or unverified (its\n section holds a Must, or `<m>` is above zero in step 5), the next\n round is `whole`, even when the owner dropped that Must: one\n `accord-critic` in a fresh context, no lens, mode `plan`, with the\n paths of the ticket, `plan.md`, and `plan-review.md`. After any other\n round the next is `diff`: write\n `git diff <that round's commit> -- accord/tickets/<id>/plan.md`, which\n holds the planner's fix, into a file outside the working tree, and\n dispatch one `accord-critic` in a fresh context, mode `diff`, with the\n paths of the ticket, `plan.md`, that file, and `plan-review.md`. A\n `diff` critic never reads the whole plan.\n2. **Record.** Append the round's section to `plan-review.md` and mark\n each new finding `open`. Mark `fixed` each finding a critic lists\n after `fixed:`, and `withdrawn` each one it lists after `withdrawn:`.\n When a new finding starts `repeats <round>.<n>:`, mark that earlier\n finding `repeated as <this round>.<n>`. A Nice goes to the ledger\n under `## Deferred`, marked `deferred`, and never to the planner.\n3. **Owner.** A finding whose proposed fix relies on an API cited only\n by a URL outside the repository goes to the owner at once, as\n `./ask.md` says: show the finding, the fix, and the URL, and ask\n whether to apply or drop it. On apply its state becomes\n `open, owner: \"<the owner's words>\"`: it goes to the planner with\n those words, which confirm the API, and closes only when a critic\n lists it after `fixed:`. On drop its state becomes\n `owner: \"<the owner's words>\"`, which closes it.\n4. **Converged.** The loop ends only on a round that reports no Must\n and no Should and leaves no Must or Should open or unverified, and\n only when the last `accord gate plan <id>` passed: go to step 7 of\n the skill.\n5. **Fix.** Otherwise commit `plan.md` and `plan-review.md` with the\n subject `plan: <id> round <n>, <m> Must, <s> Should`, where `<m>` and\n `<s>` count the Musts and the Shoulds after the last `## Closed` line\n that are `open` or `unverified` once this round is recorded; the next\n `diff` round reads from that commit. Dispatch `accord-planner` with the\n ticket path, the baseline, the mode, the path of `plan-review.md`, and\n every Must and Should that is `open` or `unverified`, never a Nice,\n each labelled `<round>.<n>` and listed with the owner's words it holds.\n The planner fixes every one and replies by that label. Write each\n `unverified: <reason>` it replies into the state of the finding with that\n label; a finding it reports fixed stays `open` until a critic lists it\n after `fixed:`. Then go back to step 4 of the skill.\n",
24964
- "review/SKILL.md": "---\nname: accord-review\ndescription: Use when every task of a lane M ticket has a ledger line and the work needs review. Dispatches reviewers on the review package, fixes patch findings through fresh executors until no Must is open and every Should is fixed, runs one final reviewer, and records each round in review.md.\nlicense: MIT\nkind: skill\nloads: [review/record.md, shared/ask.md]\n---\n\nThis stage reviews the built work against the ticket until it converges.\nYou run it in the main session and orchestrate: reviewers find, executors\nfix.\n\n## Steps\n\n1. Run `accord anchor`.\n\n2. **Package.** Run `accord review-package <id>` into a file outside the\n working tree. It holds the commits, the stat, and the diff from the\n plan's `baseline:`, and lists under `## Undeclared files` every changed\n file no task's `files:` line names. The review unit is the whole\n ticket, whatever `review.mode` in `accord/config.yml` says. When\n `accord anchor` prints `Review base: <sha>`, run\n `accord review-package <id> --base <sha>` instead.\n\n3. **Reviewers.** Choose by the unit's diff size and risk:\n - up to about 500 changed lines: one `accord-reviewer`, lens `both`;\n - about 500 to 1500: an `intent` and a `quality` reviewer, in parallel;\n - over about 1500: split by area times lens, plus one `integration`\n reviewer reading only the interfaces and links between areas;\n - auth, money, migration, or data deletion: add a `security` reviewer,\n at any size.\n\n Name the lens in each dispatch. Every reviewer gets the paths of the\n ticket file, the review package, and `accord/tickets/<id>/ledger.md`.\n The executor's report is context, never evidence.\n\n4. **Classify.** De-duplicate the findings, then mark each one `patch`, `plan-gap`, or `intent-gap`.\n - Fix every Must and Should marked `patch` through `accord-build`'s\n executor path. Note the HEAD sha. Write a brief file outside the\n working tree: copy the ticket file byte for byte (never retype it),\n then append the findings to fix. Dispatch a fresh `accord-executor`\n with its path, and route its stops as `accord-build` does.\n - When it reports, run the `tests.affected` command over the files the\n fix touched, or `tests.full` when `tests.affected` is not configured or\n finds no test file. If it fails, dispatch a fresh executor with the same\n brief plus the failing output, in the same round.\n - Copy every Ruling in its report, and each file outside the plan you\n accept as a Ruling naming the file and its reason, into\n `accord/tickets/<id>/ledger.md` under `## Rulings`; commit the ledger.\n - When the round reported a Must, re-review only the fix diff:\n `accord review-package <id> --base <the noted sha>` into a file\n outside the working tree, one reviewer, lens `both`. When it reported\n only Should findings, no reviewer is dispatched again for that fix:\n that command passing checks it.\n - A plan gap goes back to `accord-plan` as a re-plan, where the critic\n runs again.\n - An intent gap stops the run and asks the owner.\n - Nice findings go to the ledger under `## Deferred`, not fixed.\n\n5. **Converged** means no Must is open and every Should is fixed with that\n command passing. If round 3 still finds\n a new Must, say whether it traces to the ticket or the plan; if it\n does, ask the owner before round 4.\n\n6. **Cap.** At most five rounds, after verify counted from the first round after it. A round at or after\n `escalation.fix_round_tier_up` in `accord/config.yml` (default 4)\n dispatches a fresh executor one model tier above the executor's\n configured model, passed as `model` on the dispatch itself: `haiku`,\n then `sonnet`, then `opus`; `opus` stays `opus`. At the cap, stop and\n report the open findings to the owner; never report the work finished.\n\n7. **Final review.** After convergence, one fresh `accord-final-reviewer`\n reads the whole review package once (over about 1500 changed lines,\n dispatch it with lens `integration`). If it finds a Must:\n one fix and one scoped re-review, as in step 4. If it finds only Should\n findings, fix them and run that command as in step 4, with no\n re-review. If a Must is still\n open after that, stop and ask the owner.\n\n8. **Record.** Write every round to `accord/tickets/<id>/review.md` as\n `./record.md` says, ending `verdict: converged`, or, when the owner\n accepts any stop with findings still open, `verdict: accepted-by-owner`\n followed by the owner's sentence quoted.\n Commit it.\n\n9. Next: `accord-verify`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary:\n an executor gets it inside its brief.\n- The main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
24965
- "review/record.md": "---\nkind: reference\n---\n\n# The review record\n\n`accord/tickets/<id>/review.md` holds one section per review round, in\norder, and ends with the verdict.\n\n```\n## Round <n>\n\nMust|Should|Nice <n> [patch|plan-gap|intent-gap]: <finding> — <file:line> — fixed in <sha> | deferred | open\n```\n\n- One `## Round <n>` section per round, the final review included, each\n listing that round's findings after merging and de-duplication, numbered\n within the round.\n- Each finding carries its severity, its class, what it is, where it is,\n and what became of it: `fixed in <sha>` with the fix commit, `deferred`\n for a Nice finding written to the ledger under `## Deferred`, or `open`.\n- A round with no findings says `No findings`.\n\nThe single last line of the file is the verdict:\n\n- `verdict: converged` when, after the final review, no Must is open and\n every Should is fixed with that command passing: `tests.affected` over\n the files the fix touched, or `tests.full` when `tests.affected` is not\n configured or finds no test file;\n- `verdict: accepted-by-owner \"<the owner's sentence>\"` when the run\n stopped with findings still open, at the cap or after the final\n re-review, and the owner accepted them, quoting what the owner said.\n\nNothing follows the verdict line.\n",
25254
+ "review/SKILL.md": "---\nname: accord-review\ndescription: Use when every task of a lane M ticket has a ledger line and the work needs review. Dispatches reviewers on the review package, fixes patch findings through fresh executors until no Must is open and every Should is fixed, runs one final reviewer, and records each round in review.md.\nlicense: MIT\nkind: skill\nloads: [review/record.md, shared/ask.md]\n---\n\nThis stage reviews the built work against the ticket until it converges.\nYou run it in the main session and orchestrate: reviewers find, executors\nfix.\n\n## Steps\n\n1. Run `accord anchor`.\n\n2. **Package.** Run `accord review-package <id>` into a file outside the working tree. It holds the\n commits, the stat, and the diff from the plan's `baseline:`, and lists under `## Undeclared files`\n every changed file no task's `files:` line names, and under `## Interface signs` each sign of a new\n interface the branch adds. The review unit is the whole ticket, whatever `review.mode` in\n `accord/config.yml` says. When `accord anchor` prints `Review base: <sha>`, run\n `accord review-package <id> --base <sha>` instead.\n\n3. **Reviewers.** Choose by the unit's diff size and risk:\n - up to about 500 changed lines: one `accord-reviewer`, lens `both`;\n - about 500 to 1500: an `intent` and a `quality` reviewer, in parallel;\n - over about 1500: split by area times lens, plus one `integration` reviewer reading only the\n interfaces and links between areas;\n - auth, money, migration, or data deletion: add a `security` reviewer, at any size.\n\n Name the lens in each dispatch. Every reviewer gets the paths of the ticket file, the review\n package, and `accord/tickets/<id>/ledger.md`. The executor's report is context, never evidence.\n\n4. **Classify.** De-duplicate the findings, then mark each one `patch`, `plan-gap`, or `intent-gap`.\n - Fix every Must and Should marked `patch` through `accord-build`'s executor path. Note the HEAD sha.\n Write a brief file outside the working tree: copy the ticket file byte for byte (never retype it),\n then append the findings to fix. Dispatch a fresh `accord-executor` with its path, and route its\n stops as `accord-build` does.\n - When it reports, run the `tests.affected` command over the files the fix touched, or `tests.full`\n when `tests.affected` is not configured or finds no test file. If it fails, dispatch a fresh\n executor with the same brief plus the failing output, in the same round.\n - Copy every Ruling in its report, and each file outside the plan you accept as a Ruling naming the\n file and its reason, into `accord/tickets/<id>/ledger.md` under `## Rulings`; commit the ledger.\n - When the round reported a Must, re-review only the fix diff:\n `accord review-package <id> --base <the noted sha>` into a file outside the working tree, one\n reviewer, lens `both`. When it reported only Should findings, no reviewer is dispatched again for\n that fix: that command passing checks it.\n - A plan gap goes back to `accord-plan` as a re-plan, where the critic runs again.\n - An intent gap stops the run and asks the owner.\n - Nice findings go to the ledger under `## Deferred`, not fixed.\n\n5. **Converged** means no Must is open and every Should is fixed with that command passing. If round 3 still finds a new\n Must, say whether it traces to the ticket or the plan; if it does, ask the owner before round 4.\n\n6. **Cap.** At most five rounds, after verify counted from the first round after it. A round at or after\n `escalation.fix_round_tier_up` in `accord/config.yml` (default 4) dispatches a fresh executor one model tier above the\n executor's configured model, passed as `model` on the dispatch itself: `haiku`, then `sonnet`, then `opus`; `opus` stays\n `opus`. At the cap, stop and report the open findings to the owner; never report the work finished.\n\n7. **Final review.** After convergence, one fresh `accord-final-reviewer` reads the whole review package once (over about\n 1500 changed lines, dispatch it with lens `integration`). If it finds a Must: one fix and one scoped re-review, as in\n step 4. If it finds only Should findings, fix them and run that command as in step 4, with no re-review. If a Must is\n still open after that, stop and ask the owner.\n\n8. **Record.** Write every round to `accord/tickets/<id>/review.md` as `./record.md` says, ending `verdict: converged`, or,\n when the owner accepts any stop with findings still open, `verdict: accepted-by-owner` followed by the owner's sentence\n quoted. Commit it.\n\n9. Next: `accord-verify`.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary: an executor gets it inside\n its brief.\n- The main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public interface stops the run and\n asks the owner. Every other decision is a Ruling in `accord/tickets/<id>/ledger.md` under\n `## Rulings`: `Ruling: <what> — <why> — <cost if wrong>`. Read `./ask.md` before any question to the\n owner.\n",
25255
+ "review/record.md": "---\nkind: reference\n---\n\n# The review record\n\n`accord/tickets/<id>/review.md` holds one section per review round, in\norder, and ends with the verdict.\n\n```\n## Round <n>\n\nMust|Should|Nice <n> [patch|plan-gap|intent-gap]: <finding> — <file:line> — fixed in <sha> | deferred | open\n```\n\n- One `## Round <n>` section per round, the final review included, each\n listing that round's findings after merging and de-duplication, numbered\n within the round.\n- Each finding carries its severity, its class, what it is, where it is,\n and what became of it: `fixed in <sha>` with the fix commit, `deferred`\n for a Nice finding written to the ledger under `## Deferred`, or `open`.\n- A round with no findings says `No findings`.\n- Every `Confirmed wording-only: <path>` and `Not an interface: <path> — <reason>` line a reviewer reports is copied verbatim, one per line, into that round's section. Only the last `## Round` is read for them, so the last round, the final review, carries every one that still holds.\n\nThe single last line of the file is the verdict:\n\n- `verdict: converged` when, after the final review, no Must is open and\n every Should is fixed with that command passing: `tests.affected` over\n the files the fix touched, or `tests.full` when `tests.affected` is not\n configured or finds no test file;\n- `verdict: accepted-by-owner \"<the owner's sentence>\"` when the run\n stopped with findings still open, at the cap or after the final\n re-review, and the owner accepted them, quoting what the owner said.\n\nNothing follows the verdict line.\n",
24966
25256
  "shared/ask.md": "---\nkind: reference\n---\n\n# Asking the owner\n\nA question that needs context to be understood is asked in plain chat, never in\na question dialog, because the dialog hides the chat written before it: a few\nplain sentences of context, then the question, then lettered options, each\nsaying what happens if it is chosen, the recommended one first, with one\nsentence on why it is recommended. A question dialog is used only when the\nquestion and its options are understood on their own in one line, such as\n'Commit now?'.\n\nAsk multiple choice when you can list two to four exclusive options, each with\na consequence drawn from the code or the ticket; ask an open question, with no\noptions, when the answer is in the owner's head, such as why, for whom, what\nsuccess looks like, or what hurts today; offer a short menu only when the owner\nis stuck on an open question.\n\nExplain in plain words for an owner who reads no code: a file path or a symbol\nis only a pointer, never the explanation, and every id such as ac-2 or D-03\ncarries a short gloss.\n",
24967
25257
  "shared/failure.md": "---\nkind: reference\n---\n\n# A failure found after verify\n\nA failure is found after verify when ticket <id> has reached verify\n(`accord anchor` prints `verify` or `done`, or the ticket has\na `verified_commit:` or an `accord/tickets/<id>/verification.md`, so a\nticket back at `review` for an earlier fix counts), its pull request is\nnot merged (`gh pr view <branch> --json state` does not print `MERGED`),\nand either\nsomeone reports a failure in its work, with or without the failing\ncommand and its output, or verify finds a failed check on that pull\nrequest. A failure after the merge is not one: it needs a new bug ticket.\n\nA lane S ticket (`accord anchor` prints `Lane: S`) never reaches verify, so a failure in its work is not one: continue it as the\naccord skill's lane S steps say, and the fix gets another review round.\n\n1. Never judge the cause yourself, and never dispatch a fix outside\n `accord-debug`: no executor, no edit, and no Ruling in place of a\n root cause.\n2. When this session is not in that ticket's worktree, first run\n `accord worktree open <id> --continue` and follow the accord skill's\n `./worktree.md`, then Continue's step 2 only; after a `stop:` line,\n ask the owner to type, in the session opened there, `/accord <id> `\n followed by the report, in place of the line it prints, and stop here.\n When it printed `enter:`, or this session is already in that worktree,\n in place of Continue's steps 3 and 4, load\n `accord-debug` for that ticket: a new episode, or the open one when\n the last `## ` heading of\n `debug.md` is not `## Resolved`. Pass the debugger the report as given;\n when it names no failing command or output, say so, and the debugger\n finds them itself, for a failed check with\n `gh run view <run-id> --log-failed`.\n3. A failure only another operating system shows (a CI check, or a test\n skipped here): its red is the CI log, and its green is that check\n passing at the next verify. Give the debugger, and the executor in\n its brief, the failing check's log as the failing output, and say\n the reproduction test is skipped on this operating system. After\n the fix, go on to the stage `accord anchor` prints, without\n `## Resolved` (`done`: `accord-verify` at step 6, whose Finish\n closes the episode). The episode stays open\n across that verify and keeps its count of failed fixes: the check\n passing there resolves it, the check failing there counts one failed\n fix in it.\n4. After the fix, `accord anchor` prints `review` and a `Review base:`\n line: the fix gets its own review round, then verify runs again.\n",
25258
+ "shared/finish.md": "---\nkind: reference\n---\n\n# Reporting a finished ticket\n\nThe last message of a finished ticket has three parts, in this order, and nothing else.\n\n1. One verdict line: `✅ <id> DONE — PR #<n> ready, waiting for your merge: <url>`, with `<n>` and `<url>` from\n `gh pr list --head <branch> --state open --json number,url`; a merged pull request is not open. When no pull request is open, because `gh` is missing or the owner\n is to open it: `✅ <id> DONE — branch <branch> pushed, open its pull request`. Never merge the pull request\n and never write that it is merged: merging waits for the owner.\n2. At most four status lines: the facts the owner needs about this finish, such as the tests, CI, the review's\n verdict, and what was skipped or not checked. The closing lines on what was skipped go here, within this limit.\n3. `Next:`, then one to three suggestions. Run `accord dashboard` now and read its last line: each suggestion,\n one per line, is an action it names, written as its command: `[Continue <id>]` as `/accord continue <id>`,\n `[Pick up <id>]` as `/accord pick-up <id>`, `[New request]` as `/accord <request>`. Leave out an action on the\n ticket just finished. The first ends with `(recommended)`. Never suggest an action the dashboard does not print.\n",
24968
25259
  "shared/prototype.md": "---\nkind: reference\n---\n\n# Producing a prototype\n\nA `ui: true` ticket needs something a reviewer can look at before the code\nexists. In the `maintain` profile that something is\n`accord/assets/<id>/prototype.html`, and it is derived from what the project\nalready looks like — not from a fresh idea of what it could look like.\n\n## 1. Find the styles the project already has\n\nBefore writing any markup, find where the look of this project is defined.\nLook for, in roughly this order:\n\n- the path named under `design.tokens` in `accord/config.yml`, if one is set;\n- a stylesheet or variables file the application loads on every page;\n- a utility-class configuration, if the project styles that way;\n- a components or design-system folder.\n\nRead what you find. You are looking for the names the project uses for its\ncolours, spacing, radii, and type — those names are what the prototype spends,\nand inventing new ones is a redesign the ticket did not ask for.\n\nIf you find nothing, say so in the ticket's `## Open questions` and keep\ngoing: a prototype built from nothing has to record what it was built from\ninstead.\n\n## 2. Start from a single HTML file\n\nOne HTML file: a comment block at the top, an empty body. The comment block\nnames the ticket, a `Derived from:` line for what step 1 found, and the owner.\nBuild the page underneath it. One file, no build step, no assets directory —\na reviewer opens it in a browser and it works.\n\n## 3. Put it where the gate looks\n\n`accord/assets/<id>/prototype.html`, with `<id>` the ticket id. That folder is\nthis stage's own; nothing else writes into it.\n\n## 4. Run `accord lint` and fix what it reports\n\nDo not work from memory of the rules — run it, read the reasons, and fix what\nit names.\n\nA finding you disagree with is a question, not a licence: write it into the\nticket's `## Open questions` and stop, rather than working around the rule.\n\n## 5. Re-run the gate\n\n`accord gate ready <id>` is the only thing that says whether the ticket now\nhas what it was missing.\n",
24969
- "verify/SKILL.md": "---\nname: accord-verify\ndescription: Use when a lane M ticket's review.md ends with a verdict and the work needs verifying. Runs the full suite, has a fresh verifier write verification.md, builds the owner's report, and runs accord gate done once the owner has approved verified.\nlicense: MIT\nkind: skill\nloads: [verify/ci.md, accord/ids.md, accord/worktree.md, shared/ask.md, shared/failure.md]\n---\n\nThis stage checks the built work against the ticket and hands it to the\nowner. You run it in the main session and orchestrate: the verifier\njudges, the owner decides.\n\n## Steps\n\n1. Run `accord anchor`.\n\n2. **Merge and suite.** First commit everything, `review.md` included, so\n the working tree is clean. Then run\n `accord worktree merge <id>`. It fetches, and when the ticket branch\n lacks commits of the remote's default branch it merges them with a\n merge commit, never a rebase. Show a line starting\n `fetch from origin failed` verbatim and go on. When it exits 2 (a\n conflict it aborted, a failed merge, or a refusal), show the message\n and stop. The owner resolves a conflict, or you do when the owner says\n so, and then this stage starts again. Start CI as `./ci.md`\n says, then run the `tests.full` command from `accord/config.yml` at the\n commit now checked out, so the report `tests.report` names is fresh:\n that commit is the one verified. Right after it, before the verifier,\n run `accord tests skipped` and keep what it prints.\n\n3. **Verifier.** Dispatch `accord-verifier` in a fresh context with the\n path of `accord/tickets/<id>.md`. It writes\n `accord/tickets/<id>/verification.md` and nothing else.\n\n4. **Report.** Wait for CI as `./ci.md` says first; a failed check goes to\n `./failure.md` instead of this report. Build the owner's report in chat\n from `verification.md`, the report `tests.report` names, and\n `accord/tickets/<id>/ledger.md`:\n - one line per criterion: pass or fail, with its evidence (the test and\n its output, or a screenshot for UI);\n - CI: each check's result, or CI not checked and why;\n - three to five \"try this\" steps that take the owner about two minutes;\n - every Ruling under the ledger's `## Rulings`, ordered by its\n cost if wrong, highest first;\n - the deferred items under the ledger's `## Deferred`;\n - what was not checked, including every criterion the verifier\n recorded as `blocked`, every check CI did not check, and every test\n `accord tests skipped` printed, by name, with the operating system it\n names;\n - slow tests: the slowest cases in the report, with their times.\n\n5. **Owner.** Read `review.mode` in `accord/config.yml` (absent counts as\n `human`).\n - Under `review: outcome`, after the report, ask the owner one\n question with two choices, approve or not yet. The question names\n the criteria to tick, the commit verified, and the three values you\n will write. Only on an explicit approve, write `verified:` with those\n criteria, `verified_hash:` set to the current `ac_hash:`, and\n `verified_commit:` to the commit verified. Silence, an unclear\n answer, or a yes given before the report is not an approve: ask\n again. `## Verification notes` is not required.\n - Under `review: human` the 0.1.0 human layer stays: a person ticks\n `verified:` by hand, with the same two values, and writes\n `## Verification notes`. You never write any of them.\n - A `blocked` criterion: show the owner, who decides. A `fail` criterion,\n or a failure the owner names in answer, goes to `./failure.md`, never\n to `accord-build` directly; it is never ticked past.\n\n6. Run `accord gate done <id>` and work from what it prints. Leave\n `verification.md` and the tick uncommitted until it passes;\n the notice that the review file has no author yet is expected and no\n reason to commit earlier. When it passes, read the ticket's\n `## Corrections` and the Rulings under the ledger's `## Rulings`. For\n each one you judge to hold across tickets, ask the owner whether it\n becomes a rule of this repository; on a yes, write it as one rule\n line in `accord/product/conventions.md`. Without the owner's yes,\n write nothing to `conventions.md`. Then commit only\n `accord/tickets/<id>.md`, `accord/tickets/<id>/verification.md` and,\n when a rule was written, `accord/product/conventions.md`; under\n `review: outcome` the commit message quotes the owner's answer.\n When both files are already committed and no rule was written,\n commit nothing. Then finish CI as `./ci.md` says.\n\n7. Next: `/accord`, for the next ticket.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary.\n- The main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
25260
+ "verify/SKILL.md": "---\nname: accord-verify\ndescription: Use when a lane M ticket's review.md ends with a verdict and the work needs verifying. Runs the full suite, has a fresh verifier write verification.md, builds the owner's report, and runs accord gate done once the owner has approved verified.\nlicense: MIT\nkind: skill\nloads: [verify/ci.md, accord/ids.md, accord/worktree.md, shared/ask.md, shared/failure.md, shared/finish.md]\n---\n\nThis stage checks the built work against the ticket and hands it to the\nowner. You run it in the main session and orchestrate: the verifier\njudges, the owner decides.\n\n## Steps\n\n1. Run `accord anchor`.\n\n2. **Merge and suite.** First commit everything, `review.md` included, so\n the working tree is clean. Then run\n `accord worktree merge <id>`. It fetches, and when the ticket branch\n lacks commits of the remote's default branch it merges them with a\n merge commit, never a rebase. Show a line starting\n `fetch from origin failed` verbatim and go on. When it exits 2 (a\n conflict it aborted, a failed merge, or a refusal), show the message\n and stop. The owner resolves a conflict, or you do when the owner says\n so, and then this stage starts again. Start CI as `./ci.md`\n says, then run the `tests.full` command from `accord/config.yml` at the\n commit now checked out, so the report `tests.report` names is fresh:\n that commit is the one verified. Right after it, before the verifier,\n run `accord tests skipped` and keep what it prints.\n\n3. **Verifier.** Dispatch `accord-verifier` in a fresh context with the\n path of `accord/tickets/<id>.md`. It writes\n `accord/tickets/<id>/verification.md` and nothing else.\n\n4. **Report.** Wait for CI as `./ci.md` says first; a failed check goes to\n `./failure.md` instead of this report. Build the owner's report in chat\n from `verification.md`, the report `tests.report` names, and\n `accord/tickets/<id>/ledger.md`:\n - one line per criterion: pass or fail, with its evidence (the test and\n its output, or a screenshot for UI);\n - CI: each check's result, or CI not checked and why;\n - three to five \"try this\" steps that take the owner about two minutes;\n - every Ruling under the ledger's `## Rulings`, ordered by its\n cost if wrong, highest first;\n - the deferred items under the ledger's `## Deferred`;\n - what was not checked, including every criterion the verifier\n recorded as `blocked`, every check CI did not check, and every test\n `accord tests skipped` printed, by name, with the operating system it\n names;\n - slow tests: the slowest cases in the report, with their times.\n\n5. **Owner.** Read `review.mode` in `accord/config.yml` (absent counts as\n `human`).\n - Under `review: outcome`, after the report, ask the owner one\n question with two choices, approve or not yet. The question names\n the criteria to tick, the commit verified, and the three values you\n will write. Only on an explicit approve, write `verified:` with those\n criteria, `verified_hash:` set to the current `ac_hash:`, and\n `verified_commit:` to the commit verified. Silence, an unclear\n answer, or a yes given before the report is not an approve: ask\n again. `## Verification notes` is not required.\n - Under `review: human` the 0.1.0 human layer stays: a person ticks\n `verified:` by hand, with the same two values, and writes\n `## Verification notes`. You never write any of them.\n - A `blocked` criterion: show the owner, who decides. A `fail` criterion,\n or a failure the owner names in answer, goes to `./failure.md`, never\n to `accord-build` directly; it is never ticked past.\n\n6. Run `accord gate done <id>` and work from what it prints. Leave\n `verification.md` and the tick uncommitted until it passes;\n the notice that the review file has no author yet is expected and no\n reason to commit earlier. When it passes, read the ticket's\n `## Corrections` and the Rulings under the ledger's `## Rulings`. For\n each one you judge to hold across tickets, ask the owner whether it\n becomes a rule of this repository; on a yes, write it as one rule\n line in `accord/product/conventions.md`. Without the owner's yes,\n write nothing to `conventions.md`. Then commit only\n `accord/tickets/<id>.md`, `accord/tickets/<id>/verification.md` and,\n when a rule was written, `accord/product/conventions.md`; under\n `review: outcome` the commit message quotes the owner's answer.\n When both files are already committed and no rule was written,\n commit nothing. Then finish CI as `./ci.md` says.\n\n7. Report the finish as `./finish.md` says.\n\n## Rules\n\n- Every agent receives the ticket verbatim, by file path, never a summary.\n- The main session orchestrates and never edits product code.\n- Anything touching intent, a veto, an acceptance criterion, or a public\n interface stops the run and asks the owner. Every other decision is a\n Ruling in `accord/tickets/<id>/ledger.md` under `## Rulings`:\n `Ruling: <what> — <why> — <cost if wrong>`.\n Read `./ask.md` before any question to the owner.\n",
24970
25261
  "verify/ci.md": "---\nkind: reference\n---\n\n# CI on the pull request\n\nVerify watches the pull request's CI once, while its own checks run. Only\nGitHub Actions on a GitHub pull request is read; anything else is\nreported as CI not checked, with the reason.\n\n## Start: step 2, before the suite\n\n1. Read `git remote get-url origin`. No `origin`, or a host other than\n `github.com`: CI not checked, \"no GitHub remote\"; skip the rest of Start.\n2. Run `gh auth status`, then\n `gh pr list --head <branch> --state open --json number,url`. gh\n missing, not logged in, or unable to reach the repository: CI not\n checked, with gh's message; skip the rest of Start.\n3. Run `git push -u origin <branch>`. A rejected push: CI not checked,\n with git's message; skip the rest of Start. Note `<sha>`, from\n `git rev-parse HEAD`, and the time. A push that changed nothing still\n counts: the runs already on `<sha>` are used.\n4. When that list was empty, run `gh pr create --draft --fill`. A pull\n request already open is left as it is. Never merge it: merging waits\n for the owner.\n\nThen run the suite and the verifier while CI runs.\n\n## Wait: step 4, before the report\n\n- When `accord/tickets/<id>/debug.md` has an open episode (its last\n `## ` heading is not `## Resolved`) and Wait is skipped or ends with\n its checks not checked, the report's not-checked line says \"the fix\n for episode <n> was not seen passing on the other operating system\".\n- When Start was skipped, skip Wait; the report gives Start's reason.\n- When `.github/workflows/` holds no workflow file other than\n `accord.yml`: CI not checked, \"no workflow other than accord's\"; skip\n the rest of Wait.\n- Only runs on `<sha>` count:\n `gh run list --commit <sha> --json databaseId,workflowName,status,conclusion`.\n Leave out the workflow whose `name:` is in `.github/workflows/accord.yml`:\n never wait for it and never send it to debug; step 6's local checks\n stand in for it.\n- Read the list once a minute until a run appears. None within ten\n minutes of the push: CI not checked, \"no check appeared on `<sha>`\n within ten minutes\".\n- Then read it once a minute until every run is `completed`. A run not\n completed thirty minutes after it appeared: CI not checked for it, by\n name, \"still running after thirty minutes\".\n- A check is a job: for each completed run,\n `gh run view <run-id> --json jobs` lists its jobs, and each job is one\n check, named `<workflowName> / <job name>`, with the job's\n `conclusion`. A run with no jobs is one check under its workflow name,\n with the run's `conclusion`.\n- Once every run has finished: `success` passed; `failure`, `timed_out`\n and `startup_failure` failed; any other conclusion, `cancelled` and\n `skipped` among them, is CI not checked for that check, by name, and\n goes to no debug.\n- Any failed check: give the owner one line, \"CI failed on `<sha>`:\n <the failed checks' names>\", show no report, ask for no tick, and\n follow `./failure.md` with the ids of the runs those checks belong to.\n- Every check passed and `accord/tickets/<id>/debug.md` has an open\n episode (its last `## ` heading is not `## Resolved`): append\n `## Resolved` with one line naming `<sha>` and the passing checks, and\n commit `debug.md` alone.\n- Otherwise the report gives each check's name and result.\n\n## Finish: step 6, after the tick is committed\n\n1. When `debug.md` still has an open episode, append `## Resolved`\n with one line, \"the owner's decision: ticked with CI not checked\",\n and commit `debug.md` alone.\n2. Run `accord lint`. `<def>` is the first of `origin/HEAD`,\n `origin/main` and `origin/master` that `git rev-parse --verify -q`\n resolves. Run `accord gate done <t>` for every ticket file\n `git diff --name-only --diff-filter=d <def>...HEAD` lists as\n `accord/tickets/<t>.md`; when none resolves, run\n `accord gate done <id>` alone. Any failure: show it and stop; nothing\n is pushed.\n3. Run `git status --porcelain --branch`. When it names an upstream on\n `origin` and `origin` is a GitHub remote: when it shows the branch\n ahead, run `git push`; then, unless\n `gh pr view --json isDraft --jq .isDraft` prints `false`, run\n `gh pr ready`. Do not wait for CI again. This step also finishes a\n session that stopped after the tick commit.\n"
24971
25262
  };
24972
25263
  const options$1 = {
@@ -25104,7 +25395,7 @@ const agents = {
25104
25395
  "debugger.md": "---\nname: accord-debugger\ndescription: Finds the root cause of a failure with competing hypotheses, ranked before any is tested, and records the investigation in debug.md, for the debug stage. Never fixes anything.\ntools: Read, Grep, Glob, Bash, Write\nroute: debugger\n---\n\nYou find the root cause of one failure. You never fix it.\n\n- Your input is the path of the ticket file, `accord/tickets/<id>.md`, and the failing command with its output, or a failure report that gives neither: then find the failing command and its output yourself, for a failed CI check with `gh run view <run-id> --log-failed`. Read all of the ticket. On a later round you also get the path of `accord/tickets/<id>/debug.md`; read it first. You have no chat history.\n- You read and run commands, and run `git` only to read. You write one file: `accord/tickets/<id>/debug.md`.\n\nHow you work:\n1. Read the error completely and reproduce it; a failure that only another operating system shows is reproduced by its CI log. Not reproducible means gather more data, never guess. Check what changed: `git diff`, recent commits, config.\n2. Write competing hypotheses, each \"X causes it because Y\", each with its evidence for and its evidence against. One hypothesis is always that the test or measurement itself is wrong.\n3. Rank them before you test any.\n4. Run first the cheapest probe that separates the top two. A probe is a command or a read, one variable each; a probe that needs a code change you propose, never make. Record each probe and what it showed, then re-rank.\n5. Trace a bad value back to where it starts, not where it surfaces, and grep every caller of the code at fault: the root cause is fixed once, in the code every caller routes through.\n6. When you do not know, write \"I do not understand X\". Never narrate a guess as a cause.\n\nWrite `debug.md` in this format, under its last `## Episode <n>` heading, keeping everything above it:\n\n```markdown\n## Symptom\n<the failing command and its output>\n\n## Hypotheses\n1. <X causes it because Y> — for: <evidence> — against: <evidence>\n\n## Probes\n- <command or read> → <what it showed> → <hypotheses it separated>\n\n## Fixes\n- <fix n> — <result>\n\n## Root cause\n<the cause, with path:line and the evidence>\n```\n\nKeep `## Fixes` as you found it; the debug stage writes it.\n\nReturn, at most 15 lines: the root cause with its evidence, the failing reproduction test you propose (its file, its name, and what it asserts), and the hypotheses ruled out with what ruled each out. Without evidence, return your ranked hypotheses and say no root cause is shown.\n\nWhen you are dispatched with one hypothesis in a parallel round, you test only that hypothesis, write nothing, and return its evidence for and against and what you ran. The debug stage merges every result into `debug.md`, so parallel debuggers never write one file.\n\nYou never edit product code, tests, the ticket, or the ledger, never propose a fix beyond the reproduction test, and never dispatch another agent.\n",
25105
25396
  "executor.md": "---\nname: accord-executor\ndescription: Builds the one plan task or fixes the review or debug findings its brief names, failing test first, and reports in at most 15 lines. Never edits the ticket or plan.md.\nroute: executor\n---\n\nYou build what your brief names, test first.\n\n- Your input is the path of one brief file: the ticket verbatim, then either one task from `plan.md` or the review or debug findings to fix. Read all of it. On a dispatch after a stop you also get the diff and the ledger so far, or the answer to your question. You have no chat history.\n- You read, write code, run commands, and commit.\n- Read `accord/product/` before you start, and follow `accord/product/conventions.md` in every commit and every file you write.\n- You never run `accord task-done`, and you never create, change, delete or rename any file under the repository's top-level `accord/` folder. An `accord/` folder inside a test fixture or an example is not it.\n\nHow you work:\n- Write the failing test first: the task's criterion test, or the reproduction or failing case the findings name. Show it failing, then write the code and show it passing. When the brief says the reproduction test is skipped on this operating system, the CI log the brief gives is its failing output, and the run that skips it here is its passing output. Both outputs go in your report.\n- `(@ac-n)` on a verify line is a marker, not part of the test name. A test name never holds `@` or a comma. A test whose verify id carries no marker is named with `risk:<kind>`, the kind from the\n reviewer's list (parsing, money or calculation, security or permission, concurrency, data loss, complex\n branching, regression of a real bug).\n- A `verify:` id must equal, character for character, the id accord builds from the JUnit report: the testcase's classname, then `#`, then its name, which already carries every enclosing describe title. The whole classname and the whole name are each trimmed, and each run of whitespace in them becomes `-`. When the classname attribute is missing or is the empty string before trimming, the id is that name alone.\n- Before you report `DONE`, read the JUnit report and find each verify id of your task in it; an id you cannot find there is not done.\n- An assertion that should run only on some operating systems goes in a test of its own, skipped on the others through the test runner's own skip; never behind an early return or a branch on the operating system inside a test.\n- Fix small bugs and blockers you meet on the way, and log each one in your report.\n- At most three attempts per problem; after the third, stop with `BLOCKED` and what you tried.\n- No new package without asking: that is `BLOCKED` with the package and why.\n- Declare every file you touch outside your task's `files:` line, or, for findings, outside every `files:` line in `plan.md`, with a reason.\n- Commit what you finish.\n\nStop and return `BLOCKED` with a question when the work would change architecture or a public interface, or would cross a veto.\n\nNear your context limit, never rely on compaction: commit what is finished and return `BLOCKED: context` with what remains.\n\nYour report, at most 15 lines:\n\n```\nDONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT\ncommits: <sha> <subject>, ...\nfailing: <command> → <output showing the test fail>\npassing: <command> → <output showing it pass>\nfiles: <path>, ...; outside the plan: <path> — <reason>\nfixed on the way: <bug> — <path>\nRuling: <what> — <why> — <cost if wrong>\n<the question, the concern, or what remains>\n```\n\nYou never edit the ticket or `plan.md`, and you never weaken a criterion test after showing it fail. A test that seems wrong is `BLOCKED` with the reason. You never dispatch another agent.\n",
25106
25397
  "planner.md": "---\nname: accord-planner\ndescription: Writes a ticket's plan.md, one task per section with covers, files, and verify lines, ordered wiring first and polish last, for the plan stage. Writes nothing else.\ntools: Read, Grep, Glob, Write, Edit\nroute: planner\n---\n\nYou write the plan an executor builds from, one task at a time.\n\n- You write only `accord/tickets/<id>/plan.md`. You never edit any other file.\n- You never put a file under the repository's top-level `accord/` folder on a task's `files:` line, because executors never touch it and the main session writes those files. An `accord/` folder inside a test fixture or an example is not it.\n- Your input is the path of one ticket file, the baseline commit, and the review mode, `human` or `outcome`. Read the whole ticket, its approved `## Approach` included, then the code it touches. On a revision you also get the path of `accord/tickets/<id>/plan-review.md` and the Must and Should findings to fix, never a Nice, or what `accord gate plan` printed; fix each one in `plan.md`.\n- Read `accord/product/` before you plan: its glossary, business rules and `conventions.md` bind every task.\n- A fix the critic proposes that relies on a host or library API is applied only when reading the cited place confirms that API. A fix with no citation, a citation you cannot read, or a citation that does not confirm the API is reported as `unverified: <reason>`, not applied. A finding you cannot fix without failing one of the checks below is reported as `unverified: <reason>` naming the check, not applied. A fix whose API is cited only by a URL is applied only when you are given the owner's words saying to apply it.\n- On a re-plan you also get the path of `plan.md`, the ledger, and the change note or plan gap. Keep `baseline:` as it is. Keep every task the ledger records, with its text and number; number each new or changed task after the highest existing number.\n\nThe plan:\n- Frontmatter `baseline:` is the commit your caller gives you: the output of `git rev-parse HEAD`, or, for a ticket lifted from lane S, the fork point its ledger Ruling names.\n- One `## Task <n>: <title>` section per task, each with these three lines, then prose:\n\n```\ncovers: @ac-1, D-02\nfiles: src/export.ts, src/csv.ts (new)\nverify: src/export.test.ts#writes-the-csv (@ac-1), src/csv.test.ts#risk:data-loss-quotes\n```\n\n- Every `@ac-n` and every `D-nn` in the ticket is cited by some task's `covers:`. Nothing in `covers:` is foreign to the ticket.\n- `verify:` names the test report case ids that will exist once the task's tests are written.\n- Write `(@ac-n)` after the id of the test that proves scenario `@ac-n`: each scenario that is not `@ui` is mapped\n to exactly one test; an id may carry two markers; an id that proves no scenario carries none; no `@ui` scenario\n is mapped.\n- A test whose verify id maps no scenario carries `risk:<kind>` in its name, the kind from the reviewer's list\n (parsing, money or calculation, security or permission, concurrency, data loss, complex branching, regression\n of a real bug). A test that would only repeat a criterion test at another layer is not planned.\n- A test name never holds `@` or a comma.\n- A path the task creates is followed by `(new)` on its `files:` line. Every other path must exist at the baseline.\n- The prose gives the interface (what the task consumes, what it produces) and the test written first.\n- A task that edits an existing file names the behaviour there that must not change.\n- A task fits in about half an executor's context; split anything larger.\n- Order tasks so the one that proves the approach end to end (the wiring) comes first; polish comes last.\n- A bug ticket's first task is the failing reproduction test.\n- Under review mode `human`, group the tasks into slices: each slice is one pull request covering a stated subset of the acceptance criteria, green on its own. Write each slice into `plan.md`: the tasks it holds and the criteria it covers. Under `outcome`, one branch and no slices.\n- No TODO, TBD, placeholder, \"v1\", \"for now\", or \"later\" anywhere in task text.\n\nBefore you reply, run these checks of the critic's plan mode on your plan and fix what fails:\n- Check that no Veto is violated.\n- Check that nothing goes beyond the acceptance criteria.\n- Check that the approved `## Approach` is followed.\n- Check that each task fits in half an executor's context.\n- Check that, assuming every task passes its `verify:` line, the `## Intent` holds for the person it names. A criterion covered on paper with a step nobody wired or a state nobody reaches is a Must.\n- Check that an executor holding only the ticket and one task could finish that task without asking; a task that fails this is a Should.\n\nWhen you finish, reply with the path of `plan.md`, the number of tasks, and for each finding you were given its `<round>.<n>` label, which you reply with, and `fixed` or `unverified: <reason>`.\n\nYou never create `accord/tests-baseline.txt` and never plan a task that runs `accord tests baseline`; the plan stage asks the owner about it.\n\nYou never write code, never edit the ticket, and never run a command.\n",
25107
- "reviewer.md": "---\nname: accord-reviewer\ndescription: Reviews built work through one lens, intent, quality, security, integration, or both, against the ticket, the review package, and the ledger, and reports numbered findings each with a concrete failing case. Never edits.\ntools: Read, Grep, Glob, Bash\nroute: reviewer\n---\n\nYou review built work against the ticket and find where it fails.\n\n- You read, run tests, and run `git` only to read; you never edit a file, and never check out, stash, reset, or commit.\n- Your caller names one lens: `intent`, `quality`, `security`, `integration`, or `both` (intent and quality). It gives you the path of the ticket file, the path of a review package from `accord review-package`, and the path of `accord/tickets/<id>/ledger.md`. Read all of them. The approach is the ticket's `## Approach`. On a re-review the package holds only the fix diff. A lane S ticket has no plan or acceptance criteria, and a ledger only once a Ruling is written, its path then given to you: its `## Understood` and `## Done when` are what the change must meet, and lens `intent` quotes them.\n- The executor's report, if you see one, is context, never evidence.\n\nLens `intent`:\n- Each finding quotes the criterion, veto, or correction in the ticket that it violates.\n- Extra: anything nobody asked for. A spec's silence is not permission.\n\nLens `quality`:\n- Correctness and safety.\n- Simplicity: what could be deleted, reused, or done with the standard library, and what is not needed.\n- The test policy: each acceptance criterion has exactly one behaviour test, named by its scenario's `@test:<id>` tag, at the cheapest layer that observes the behaviour; any other test carries `risk:<kind>` in its name (parsing, money or calculation, security or permission, concurrency, data loss, complex branching, regression of a real bug); no test restates the implementation, tests a getter or setter, only asserts on mocks, or tests again a behaviour a criterion test covers at another layer.\n- The repository's written conventions and its `product/` rules.\n- An early return or a branch on the operating system inside a test that leaves an assertion unrun on some operating system is a finding: that assertion belongs in a test of its own, skipped through the test runner's own skip.\n- Skip anything a configured linter or type check already enforces.\n\nLens `security`: the auth, money, migration, and data deletion paths the diff touches.\n\nLens `integration`: only the interfaces and the links between areas.\n\nUndeclared files: each path under `## Undeclared files` in the review package is accepted only when the ledger holds a Ruling naming it with a reason. Any other is an Extra finding.\n\nSeverity, by what happens if the finding is left:\n- Must: left as is, the Intent does not hold, a criterion is not reached, or a Veto, Decision or Correction is broken.\n- Should: left as is, a defect ships or an executor is blocked, which a later stage would catch at a higher cost.\n- Nice: no defect, only a clearer or smaller way.\n\nFindings:\n- Numbered, each with a severity `Must`, `Should`, or `Nice`, the lens it comes from, its file and line, and a concrete failing case: the input, step, or state, and what goes wrong.\n- No failing case, no finding.\n- When you find nothing, say `No findings`.\n\n```\n1. Must — intent — <finding> — <file:line>\n violates: \"<the criterion, veto, or correction, quoted>\"\n failing case: <input, step, or state> → <what goes wrong>\n```\n\nYou never edit a file and never fix what you find; you report it. You never dispatch another agent.\n",
25398
+ "reviewer.md": "---\nname: accord-reviewer\ndescription: Reviews built work through one lens, intent, quality, security, integration, or both, against the ticket, the review package, and the ledger, and reports numbered findings each with a concrete failing case. Never edits.\ntools: Read, Grep, Glob, Bash\nroute: reviewer\n---\n\nYou review built work against the ticket and find where it fails.\n\n- You read, run tests, and run `git` only to read; you never edit a file, and never check out, stash, reset, or commit.\n- Your caller names one lens: `intent`, `quality`, `security`, `integration`, or `both` (intent and quality). It gives you the path of the ticket file, the path of a review package from `accord review-package`, and the path of `accord/tickets/<id>/ledger.md`. Read all of them. The approach is the ticket's `## Approach`. On a re-review the package holds only the fix diff. A lane S ticket has no plan or acceptance criteria, and a ledger only once a Ruling is written, its path then given to you: its `## Understood` and `## Done when` are what the change must meet, and lens `intent` quotes them.\n- The executor's report, if you see one, is context, never evidence.\n\nLens `intent`:\n- Each finding quotes the criterion, veto, or correction in the ticket that it violates.\n- Extra: anything nobody asked for. A spec's silence is not permission.\n\nLens `quality`:\n- Correctness and safety.\n- Simplicity: what could be deleted, reused, or done with the standard library, and what is not needed.\n- The test policy: each acceptance criterion has exactly one behaviour test, named by its scenario's `@test:<id>` tag, at the cheapest layer that observes the behaviour; any other test carries `risk:<kind>` in its name (parsing, money or calculation, security or permission, concurrency, data loss, complex branching, regression of a real bug); no test restates the implementation, tests a getter or setter, only asserts on mocks, or tests again a behaviour a criterion test covers at another layer.\n- The repository's written conventions and its `product/` rules.\n- An early return or a branch on the operating system inside a test that leaves an assertion unrun on some operating system is a finding: that assertion belongs in a test of its own, skipped through the test runner's own skip.\n- Skip anything a configured linter or type check already enforces.\n\nLens `security`: the auth, money, migration, and data deletion paths the diff touches.\n\nLens `integration`: only the interfaces and the links between areas.\n\nUndeclared files: each path under `## Undeclared files` in the review package is accepted only when the ledger holds a Ruling naming it with a reason. Any other is an Extra finding.\n\nOut-of-scope files: in a lane S ticket each path under `## Out-of-scope files` in the review package is a Must finding, the ticket to be lifted to lane M, unless the ledger has the line `Wording-only: <path> — <reason>` for it. For such a path, read its diff: when it changes wording only, in at most 3 lines counted as the larger of lines added and lines removed, and the file is neither new nor deleted, write the line `Confirmed wording-only: <path>` in your report, outside any numbered finding; otherwise report a Must finding.\n\nInterface signs: each line under `## Interface signs` in the review package is a sign of a new public interface. In a lane S ticket it is a Must finding, the ticket to be lifted to lane M. In any other lane, when it adds no public interface, write the line `Not an interface: <path> — <reason>` in your report, outside any numbered finding; when it does, report a Should finding whose fix is an entry matching the path under `sensitive:` in `accord/config.yml`.\n\nSeverity, by what happens if the finding is left:\n- Must: left as is, the Intent does not hold, a criterion is not reached, or a Veto, Decision or Correction is broken.\n- Should: left as is, a defect ships or an executor is blocked, which a later stage would catch at a higher cost.\n- Nice: no defect, only a clearer or smaller way.\n\nFindings:\n- Numbered, each with a severity `Must`, `Should`, or `Nice`, the lens it comes from, its file and line, and a concrete failing case: the input, step, or state, and what goes wrong.\n- No failing case, no finding.\n- When you find nothing, say `No findings`.\n\n```\n1. Must — intent — <finding> — <file:line>\n violates: \"<the criterion, veto, or correction, quoted>\"\n failing case: <input, step, or state> → <what goes wrong>\n```\n\nYou never edit a file and never fix what you find; you report it. You never dispatch another agent.\n",
25108
25399
  "scout.md": "---\nname: accord-scout\ndescription: Reads the code and returns facts with file and line, for the intent interview and the approach's impact analysis. Never proposes, never edits.\ntools: Read, Grep, Glob\nroute: scout\n---\n\nYou answer the question you were given with facts from the code.\n\n- Every fact carries `path:line`. A claim without a location is not a fact; leave it out.\n- You read. You never edit a file, and you never propose a design or a fix.\n- When asked for impact, list every caller, test, fixture, config entry, and export that the named change would reach, each with `path:line`.\n- When something is not there, say \"not found\" and name where you looked. Never guess.\n- Your answer is at most 30 lines. Cut anything that does not answer the question.\n",
25109
25400
  "verifier.md": "---\nname: accord-verifier\ndescription: Verifies built work against each acceptance criterion of the ticket at four levels, exists, substantive, wired, and data flows, and writes verification.md. Never edits anything else.\ntools: Read, Grep, Glob, Bash, Write, Edit\nroute: verifier\n---\n\nYou verify built work against the ticket, one acceptance criterion at a time.\n\n- Your input is the path of the ticket file, `accord/tickets/<id>.md`. Read all of it. You have no chat history.\n- You read, run tests and commands, and run `git` only to read. You write one file: `accord/tickets/<id>/verification.md`.\n- You distrust summaries: the executor's report, the ledger, commit messages, and code comments are claims, never evidence. Read the code and run it.\n\nFor each criterion, check four levels:\n1. Exists: the code and the test the criterion needs are there.\n2. Substantive: it does the work, not a stub, a placeholder, or an empty branch.\n3. Wired: it is reachable from its entry point; trace the call from there.\n4. Data flows: real input reaches it and its result reaches the output, not a hard-coded value.\n\nWhat you cannot prove at all four levels is `Result: blocked`, never pass: it needs a human. A level that fails is `Result: fail`. Only a criterion proven at every level is `Result: pass`. A `@ui` scenario without a browser available is `blocked`.\n\nWrite `verification.md` in this format:\n\n```markdown\n---\nticket: \"<id>\"\ncommit: \"<git rev-parse HEAD, the commit you verified>\"\nreviewed_on: \"<today, YYYY-MM-DD>\"\n---\n\n## @ac-1 <scenario name>\n\nResult: pass\n\nEvidence: <what you ran or inspected, and what you saw>\n```\n\nOne `## @ac-n <scenario name>` block per scenario, in tag order; matching is by tag. The result is `pass`, `fail`, or `blocked`. Each label begins its line: never a list item, never indented. The evidence names the command, the path, or the output, for each of the four levels. Cite a file by its path from the repository root, with `:n` or `:n-m` after it for a line or a range of lines. \"Looks correct\" is not evidence.\n\nYou never edit any file but `verification.md`, never fix what you find, and never touch the ticket's frontmatter. You never dispatch another agent.\n"
25110
25401
  };
@@ -25561,6 +25852,80 @@ function repinWorkflow(text, name, from, to) {
25561
25852
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25562
25853
  return text.replace(new RegExp(esc(name + "@" + from) + "(?![0-9A-Za-z.+-])", "g"), () => name + "@" + to);
25563
25854
  }
25855
+ const STEP$1 = "lint and gate";
25856
+ /** Where the one step named `lint and gate` sits, as whole lines (1-based): its `- ` line to the last line of its last value. */
25857
+ function locate(src) {
25858
+ const lc = new import_dist.LineCounter();
25859
+ const doc = (0, import_dist.parseDocument)(src, {
25860
+ schema: "core",
25861
+ customTags: stringNumerics,
25862
+ lineCounter: lc
25863
+ });
25864
+ if (doc.errors.length > 0) return void 0;
25865
+ const js = doc.toJS();
25866
+ const hits = [];
25867
+ const jobs = js !== null && typeof js === "object" ? js.jobs : void 0;
25868
+ if (jobs !== null && typeof jobs === "object") for (const [job, body] of Object.entries(jobs)) {
25869
+ const steps = body?.steps;
25870
+ if (!Array.isArray(steps)) continue;
25871
+ steps.forEach((s, index) => {
25872
+ if (s !== null && typeof s === "object" && s.name === STEP$1) hits.push({
25873
+ job,
25874
+ index
25875
+ });
25876
+ });
25877
+ }
25878
+ if (hits.length !== 1) return "count";
25879
+ const { job, index } = hits[0];
25880
+ const node = doc.getIn([
25881
+ "jobs",
25882
+ job,
25883
+ "steps",
25884
+ index
25885
+ ], true);
25886
+ if (!(0, import_dist.isMap)(node) || node.flow || node.anchor !== void 0 || !node.range) return void 0;
25887
+ const [from, to] = node.range;
25888
+ const lineStart = src.lastIndexOf("\n", from - 1) + 1;
25889
+ const dash = /^( *)- +/.exec(src.slice(lineStart, from));
25890
+ if (dash === null || dash[0].length !== from - lineStart) return void 0;
25891
+ const last = src.slice(from, to).trimEnd().length;
25892
+ return {
25893
+ js,
25894
+ job,
25895
+ index,
25896
+ start: lc.linePos(from).line,
25897
+ end: lc.linePos(from + Math.max(last - 1, 0)).line,
25898
+ indent: dash[1].length
25899
+ };
25900
+ }
25901
+ /** The workflow with its one step named `lint and gate` replaced by the step `workflowYml(name, version)` writes. */
25902
+ function refreshLintStep(text, name, version) {
25903
+ try {
25904
+ const old = locate(text);
25905
+ if (old === "count") return { refused: "count" };
25906
+ const freshText = workflowYml(name, version);
25907
+ const fresh = locate(freshText);
25908
+ if (old === void 0 || fresh === void 0 || fresh === "count") return { refused: "shape" };
25909
+ const shift = old.indent - fresh.indent;
25910
+ const cut = freshText.split("\n").slice(fresh.start - 1, fresh.end).map((l) => l === "" ? l : shift >= 0 ? " ".repeat(shift) + l : l.slice(-shift));
25911
+ const lines = text.split("\n");
25912
+ const crlf = text.includes("\r\n");
25913
+ const cr = crlf && lines[old.end - 1].endsWith("\r") ? "\r" : "";
25914
+ const body = cut.join(crlf ? "\r\n" : "\n") + cr;
25915
+ const result = [
25916
+ ...lines.slice(0, old.start - 1),
25917
+ body,
25918
+ ...lines.slice(old.end)
25919
+ ].join("\n");
25920
+ const next = locate(result);
25921
+ if (next === void 0 || next === "count") return { refused: "shape" };
25922
+ const swapped = structuredClone(old.js);
25923
+ swapped.jobs[old.job].steps[old.index] = fresh.js.jobs[fresh.job].steps[fresh.index];
25924
+ return JSON.stringify(next.js) === JSON.stringify(swapped) ? { text: result } : { refused: "shape" };
25925
+ } catch {
25926
+ return { refused: "shape" };
25927
+ }
25928
+ }
25564
25929
  const panelSettings = {
25565
25930
  extraKnownMarketplaces: { accord: { source: {
25566
25931
  source: "directory",
@@ -25658,7 +26023,7 @@ function mergeHooks(existing, entries) {
25658
26023
  //#region package.json
25659
26024
  var package_default = {
25660
26025
  name: "@accord-dev/accord",
25661
- version: "0.2.0",
26026
+ version: "0.3.0",
25662
26027
  type: "module",
25663
26028
  license: "MIT",
25664
26029
  repository: {
@@ -26792,6 +27157,206 @@ function gitFilesAt(root, ref, paths) {
26792
27157
  }
26793
27158
  return files;
26794
27159
  }
27160
+ const firstLine$1 = (text) => text.trim().split("\n")[0];
27161
+ const byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
27162
+ /** A path as `git diff` prints it: a value wrapped in `"` has C-style escapes and three-digit octal bytes; any other is returned as is. */
27163
+ function unquoteGitPath(v) {
27164
+ if (v.length < 2 || !v.startsWith("\"") || !v.endsWith("\"")) return v;
27165
+ const escapes = {
27166
+ "\\": 92,
27167
+ "\"": 34,
27168
+ a: 7,
27169
+ b: 8,
27170
+ t: 9,
27171
+ n: 10,
27172
+ v: 11,
27173
+ f: 12,
27174
+ r: 13
27175
+ };
27176
+ const bytes = [];
27177
+ const inner = v.slice(1, -1);
27178
+ for (let i = 0; i < inner.length; i++) {
27179
+ const ch = inner[i];
27180
+ const oct = /^[0-7]{3}/.exec(inner.slice(i + 1, i + 4));
27181
+ if (ch !== "\\") bytes.push(...Buffer.from(ch + (inner.codePointAt(i) > 65535 ? inner[++i] : ""), "utf8"));
27182
+ else if (oct !== null) {
27183
+ bytes.push(parseInt(oct[0], 8));
27184
+ i += 3;
27185
+ } else if (inner[i + 1] in escapes) bytes.push(escapes[inner[++i]]);
27186
+ else bytes.push(92);
27187
+ }
27188
+ return Buffer.from(bytes).toString("utf8");
27189
+ }
27190
+ /**
27191
+ * ST-LN-62: the branch diff of one lane ticket, from its merge base with the default branch to HEAD, for the Done gate.
27192
+ * Never throws: each failure is an `{ error }` the gate turns into a finding. `-w` is never used, since it hides
27193
+ * whitespace removed inside a line; `--no-renames` makes a rename a deleted and an added path.
27194
+ */
27195
+ function gitBranchFact(root, id, opts) {
27196
+ const git = (what, args) => {
27197
+ try {
27198
+ return execFileSync("git", args, {
27199
+ cwd: root,
27200
+ encoding: "utf8",
27201
+ stdio: [
27202
+ "ignore",
27203
+ "pipe",
27204
+ "pipe"
27205
+ ],
27206
+ maxBuffer: 67108864,
27207
+ env: {
27208
+ ...process.env,
27209
+ GIT_NO_LAZY_FETCH: "1"
27210
+ }
27211
+ });
27212
+ } catch (err) {
27213
+ if (err.code === "ENOBUFS") throw new Error("the branch diff is larger than 64 MiB", { cause: err });
27214
+ const stderr = String(err.stderr ?? "");
27215
+ throw new Error(`git ${what} failed reading the branch diff: ${firstLine$1(stderr === "" ? err.message : stderr)}`, { cause: err });
27216
+ }
27217
+ };
27218
+ const filesAt = (ref, paths) => {
27219
+ try {
27220
+ return gitFilesAt(root, ref, paths);
27221
+ } catch (err) {
27222
+ const rest = err.message.split("\n").slice(1).join("\n");
27223
+ throw new Error(`git cat-file failed reading the branch diff: ${firstLine$1(rest === "" ? err.message : rest)}`, { cause: err });
27224
+ }
27225
+ };
27226
+ try {
27227
+ const def = defaultRefOf(root);
27228
+ if (def === void 0) return { error: "none of origin/HEAD, origin/main, origin/master, main or master resolves, so the branch diff cannot be read; run git fetch origin" };
27229
+ const base = git("merge-base", [
27230
+ "merge-base",
27231
+ def,
27232
+ "HEAD"
27233
+ ]).trim();
27234
+ const raw = git("diff", [
27235
+ "diff",
27236
+ "--raw",
27237
+ "-z",
27238
+ "--no-renames",
27239
+ "--no-ext-diff",
27240
+ base,
27241
+ "HEAD",
27242
+ "--"
27243
+ ]).split("\0");
27244
+ const changes = [];
27245
+ for (let i = 0; i + 1 < raw.length; i += 2) {
27246
+ const meta = raw[i].replace(/^:/, "").split(" ");
27247
+ const status = meta[4] ?? "";
27248
+ const kind = status === "A" ? "added" : status === "D" ? "deleted" : status === "M" ? meta[0] === meta[1] ? "modified" : "mode" : "type";
27249
+ changes.push({
27250
+ path: raw[i + 1],
27251
+ kind,
27252
+ lines: 0
27253
+ });
27254
+ }
27255
+ const counts = git("diff", [
27256
+ "diff",
27257
+ "--numstat",
27258
+ "-z",
27259
+ "--no-renames",
27260
+ "--no-ext-diff",
27261
+ "-b",
27262
+ "--ignore-blank-lines",
27263
+ "--ignore-cr-at-eol",
27264
+ base,
27265
+ "HEAD",
27266
+ "--"
27267
+ ]).split("\0");
27268
+ for (const token of counts) {
27269
+ const m = /^(\d+|-)\t(\d+|-)\t([\s\S]+)$/.exec(token);
27270
+ const change = m === null ? void 0 : changes.find((c) => c.path === m[3]);
27271
+ if (m === null || change === void 0) continue;
27272
+ if (m[1] === "-") change.binary = true;
27273
+ else change.lines = Math.max(Number(m[1]), Number(m[2]));
27274
+ }
27275
+ changes.sort((a, b) => byCodePoint(a.path, b.path));
27276
+ const patch = git("diff", [
27277
+ "-c",
27278
+ "core.quotePath=false",
27279
+ "diff",
27280
+ "-U0",
27281
+ "--no-color",
27282
+ "--no-ext-diff",
27283
+ "--no-textconv",
27284
+ "--no-renames",
27285
+ "-b",
27286
+ "--ignore-blank-lines",
27287
+ "--ignore-cr-at-eol",
27288
+ "--src-prefix=a/",
27289
+ "--dst-prefix=b/",
27290
+ base,
27291
+ "HEAD",
27292
+ "--"
27293
+ ]).split("\n");
27294
+ let current;
27295
+ let inHunk = false;
27296
+ for (const line of patch) if (line.startsWith("diff --git ")) {
27297
+ current = void 0;
27298
+ inHunk = false;
27299
+ } else if (!inHunk && line.startsWith("+++ ")) {
27300
+ const rest = line.slice(4).replace(/\t$/, "");
27301
+ const path = unquoteGitPath(rest).replace(/^b\//, "");
27302
+ if (rest === "/dev/null") continue;
27303
+ current = changes.find((c) => c.path === path);
27304
+ if (current === void 0) return { error: `git diff -U0 named ${path}, which the raw listing of the branch diff does not hold` };
27305
+ } else if (line.startsWith("@@")) inHunk = true;
27306
+ else if (inHunk && current !== void 0 && line.startsWith("+") && CALL_SIGN.test(line.slice(1).replace(/\r$/, ""))) (current.calls ??= []).push(line.slice(1));
27307
+ const pkgPaths = changes.filter((c) => c.path === "package.json" || c.path.endsWith("/package.json"));
27308
+ const pkgBase = filesAt(base, pkgPaths.filter((c) => c.kind !== "added").map((c) => c.path));
27309
+ const pkgHead = filesAt("HEAD", pkgPaths.filter((c) => c.kind !== "deleted").map((c) => c.path));
27310
+ for (const c of pkgPaths) c.pkg = {
27311
+ ...pkgBase[c.path] === void 0 ? {} : { base: pkgBase[c.path] },
27312
+ ...pkgHead[c.path] === void 0 ? {} : { head: pkgHead[c.path] }
27313
+ };
27314
+ let scope;
27315
+ if (opts.scope) {
27316
+ const file = "accord/tickets/" + id + ".md";
27317
+ const first = git("log -G ac_hash", [
27318
+ "-c",
27319
+ "log.showSignature=false",
27320
+ "log",
27321
+ "--no-show-signature",
27322
+ "--reverse",
27323
+ "--format=%H",
27324
+ "-G",
27325
+ "^ac_hash:",
27326
+ "HEAD",
27327
+ "--",
27328
+ ":(literal)" + file
27329
+ ]).split("\n").find((l) => l.trim() !== "")?.trim();
27330
+ if (first !== void 0 && isSha(first)) {
27331
+ const text = filesAt(first, [file])[file];
27332
+ scope = {
27333
+ commit: first,
27334
+ paths: text === void 0 ? [] : scopeLines(text) ?? []
27335
+ };
27336
+ }
27337
+ }
27338
+ const live = changes.filter((c) => c.kind !== "deleted").map((c) => c.path);
27339
+ const rendered = opts.rendered.filter((r) => live.includes(r.path));
27340
+ const settingsPath = opts.settings !== void 0 && live.includes(opts.settings.path) ? opts.settings.path : void 0;
27341
+ const head = filesAt("HEAD", [...rendered.map((r) => r.path), ...settingsPath === void 0 ? [] : [settingsPath]]);
27342
+ const synced = rendered.filter((r) => head[r.path] !== void 0 && normaliseText(head[r.path]) === r.text).map((r) => r.path);
27343
+ if (opts.settings !== void 0 && settingsPath !== void 0 && head[settingsPath] !== void 0) {
27344
+ const want = opts.settings.expected(filesAt(base, [settingsPath])[settingsPath]);
27345
+ if (want !== void 0 && normaliseText(head[settingsPath]) === normaliseText(want)) synced.push(settingsPath);
27346
+ }
27347
+ synced.sort(byCodePoint);
27348
+ const baseConfig = filesAt(base, ["accord/config.yml"])["accord/config.yml"];
27349
+ return {
27350
+ base,
27351
+ changes,
27352
+ synced,
27353
+ ...baseConfig === void 0 ? {} : { baseConfig },
27354
+ ...scope === void 0 ? {} : { scope }
27355
+ };
27356
+ } catch (err) {
27357
+ return { error: err.message };
27358
+ }
27359
+ }
26795
27360
  /** A SnapshotInput of `ref`'s config and tickets, read from the git tree with no checkout. */
26796
27361
  function loadRefInput(root, ref) {
26797
27362
  let listed;
@@ -27299,50 +27864,274 @@ function brief(ctx, id, task) {
27299
27864
  return 0;
27300
27865
  }
27301
27866
  //#endregion
27302
- //#region src/commands/gate.ts
27303
- function gate(ctx, which, id, options) {
27304
- const withCommits = which === "done" && ctx.snapshot.config?.commits?.trailers === "forbid" && ctx.snapshot.git !== void 0 ? {
27305
- ...ctx.snapshot,
27306
- commits: gitBranchCommits(ctx.root)
27307
- } : ctx.snapshot;
27308
- const forDone = !(which === "done" && Object.hasOwn(ctx.snapshot.tickets, id) && ctx.snapshot.tickets[id].frontmatter?.lane === "S") ? withCommits : (() => {
27309
- const after = Object.fromEntries(Object.entries(ctx.snapshot.afterVerify ?? {}).filter(([k]) => k !== id));
27310
- const found = gitAfterReview(ctx.root, id);
27311
- if (found !== void 0) after[id] = found;
27312
- return {
27313
- ...withCommits,
27314
- reviewFacts: { [id]: gitReviewFact(ctx.root, id) },
27315
- afterVerify: after
27316
- };
27317
- })();
27318
- const result = which === "ready" ? gateReady(ctx.snapshot, id) : which === "plan" ? gatePlan(ctx.snapshot, id) : gateDone(forDone, id);
27319
- ctx.stdout.write(options.json ? JSON.stringify(result, null, 2) + "\n" : colourFindings(renderText(result), ctx.stdout));
27320
- if (which === "ready" && result.verdict === "pass" && result.acHash !== void 0) {
27321
- const rel = ctx.snapshot.tickets[id].file;
27322
- try {
27323
- if (writeKey(ctx.root, rel, "ac_hash", result.acHash)) {
27324
- const prev = ctx.snapshot.tickets[id].frontmatter?.ac_hash;
27325
- ctx.stderr.write(prev === void 0 ? "wrote ac_hash " + result.acHash + " to " + rel + "\n" : "updated ac_hash " + prev + " -> " + result.acHash + " in " + rel + "\n");
27326
- }
27327
- } catch (err) {
27328
- throw new UsageError("cannot write ac_hash to " + rel + ": " + (err instanceof Error ? err.message : String(err)));
27329
- }
27330
- }
27331
- if (which === "plan" && result.verdict === "pass" && result.planHash !== void 0) {
27332
- const rel = ctx.snapshot.work?.[id]?.plan?.file;
27333
- if (rel !== void 0) try {
27334
- if (writeKey(ctx.root, rel, "plan_hash", result.planHash)) ctx.stderr.write("wrote plan_hash " + result.planHash + " to " + rel + "\n");
27335
- } catch (err) {
27336
- throw new UsageError("cannot write plan_hash to " + rel + ": " + (err instanceof Error ? err.message : String(err)));
27337
- }
27867
+ //#region src/guard.ts
27868
+ /**
27869
+ * Every component of `path` under `root` must be a real directory, and its leaf a regular file — never a
27870
+ * symlink or a Windows junction. `lstat` declines to follow only the FINAL component, so checking the leaf
27871
+ * alone leaves a junction one directory up (`.claude/skills/accord-designer`) routing every write inside it
27872
+ * to wherever the link points, with the guard never firing.
27873
+ *
27874
+ * The first component that does not exist ends the walk: `mkdirSync(..., { recursive: true })` creates real
27875
+ * directories, so nothing below it can be a link.
27876
+ *
27877
+ * Deliberately not merged with `scannable` in `commands/skills.ts`, which walks the same way. This one
27878
+ * answers "may accord WRITE here", and a link is a refusal: exit 2, nothing written. That one answers "may
27879
+ * accord LOOK here", and a link is a silent skip, because D-113 fixes that the orphan report must not change
27880
+ * the exit code. Same walk, different questions, different dispositions.
27881
+ */
27882
+ function assertNoLink(root, path) {
27883
+ const parts = path.split("/");
27884
+ for (let i = 0; i < parts.length; i++) {
27885
+ const found = lstatSync(join(root, ...parts.slice(0, i + 1)), { throwIfNoEntry: false });
27886
+ if (found === void 0) return;
27887
+ const leaf = i === parts.length - 1;
27888
+ if (leaf ? !found.isFile() : !found.isDirectory()) throw new UsageError(parts.slice(0, i + 1).join("/") + " is not a regular " + (leaf ? "file" : "directory") + " - nothing was written");
27338
27889
  }
27339
- return result.verdict === "fail" ? 1 : 0;
27340
27890
  }
27341
27891
  //#endregion
27342
- //#region src/load/proc.ts
27343
- const WIN_SCRIPT = "[Console]::OutputEncoding = [Text.Encoding]::UTF8; $r = @(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; ppid = [int]$_.ParentProcessId; start = $(if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') } else { '' }) } }); ConvertTo-Json -InputObject $r -Compress";
27344
- /** The OS process table. A failure throws: a hold is never judged on a guess. */
27345
- function processTable() {
27892
+ //#region src/commands/skills.ts
27893
+ /**
27894
+ * D-131: the one place accord writes a skill copy. `sync` and `init` both call it, so the marker-and-hash
27895
+ * rule above cannot come to mean one thing under one command and something else under the other.
27896
+ *
27897
+ * The caller owns everything around it — the `config.yml` requirement, the `assertNoLink` pre-pass, the
27898
+ * orphan scan, and the report — because those differ between the two callers and this does not.
27899
+ *
27900
+ * GC-WR-01: the caller may own the array too, so a status is recorded at the moment it becomes true rather
27901
+ * than when the loop finishes — a run that throws at target two still reports the `overwrote local edits`
27902
+ * that really happened at target one, which is the whole of what D-112 promises. The cost is that the
27903
+ * declared row type is wider than anything this function can produce: the element type belongs to the
27904
+ * report `init` prints, not to this function's own four states.
27905
+ */
27906
+ function writeSkillFiles(root, targets, out = []) {
27907
+ for (const { path, text } of targets) {
27908
+ const file = join(root, ...path.split("/"));
27909
+ if (lstatSync(file, { throwIfNoEntry: false }) === void 0) {
27910
+ mkdirSync(dirname(file), { recursive: true });
27911
+ writeFileSync(file, text);
27912
+ out.push({
27913
+ status: "created",
27914
+ path
27915
+ });
27916
+ continue;
27917
+ }
27918
+ const onDisk = normaliseText(readFileSync(file, "utf8"));
27919
+ const stored = markerHash(onDisk);
27920
+ const intact = stored !== void 0 && stored === contentHash(withoutMarker(onDisk));
27921
+ if (intact && onDisk === text) {
27922
+ out.push({
27923
+ status: "unchanged",
27924
+ path
27925
+ });
27926
+ continue;
27927
+ }
27928
+ writeFileSync(file, text);
27929
+ out.push({
27930
+ status: intact ? "updated" : "overwrote local edits",
27931
+ path
27932
+ });
27933
+ }
27934
+ return out;
27935
+ }
27936
+ const SETTINGS = ".claude/settings.json";
27937
+ /**
27938
+ * The pre-pass half: the link guard, then read, parse and merge `.claude/settings.json` for the claude runtime
27939
+ * only. The file is the owner's: anything that is not a JSON object is refused here, with the hooks printed to
27940
+ * add by hand, so the refusal lands before the first byte of the run. Writes nothing.
27941
+ */
27942
+ function planHooks(root, config) {
27943
+ if (!config.runtimes.includes("claude")) return void 0;
27944
+ assertNoLink(root, SETTINGS);
27945
+ const file = join(root, ...SETTINGS.split("/"));
27946
+ return hooksPlanFor(lstatSync(file, { throwIfNoEntry: false }) === void 0 ? void 0 : readFileSync(file, "utf8"), config);
27947
+ }
27948
+ /** ST-LN-25: the plan for a settings file whose body is `text` (undefined: it does not exist); `planHooks` minus the file read. */
27949
+ function hooksPlanFor(text, config) {
27950
+ if (!config.runtimes.includes("claude")) return void 0;
27951
+ const entries = hookEntries(package_default);
27952
+ if (text === void 0) return {
27953
+ status: "created",
27954
+ text: JSON.stringify(mergePanel(mergeHooks({}, entries)), null, 2) + "\n"
27955
+ };
27956
+ let parsed;
27957
+ let merged;
27958
+ try {
27959
+ parsed = JSON.parse(text);
27960
+ merged = mergePanel(mergeHooks(parsed, entries));
27961
+ } catch (err) {
27962
+ const keys = JSON.stringify(panelSettings, null, 2);
27963
+ if (err instanceof RangeError) throw new UsageError(SETTINGS + " already declares a marketplace named accord with another source; nothing was written. Rename it, then add these keys for the accord panel by hand:\n" + keys);
27964
+ if (!(err instanceof SyntaxError || err instanceof TypeError)) throw err;
27965
+ throw new UsageError(SETTINGS + " is not a JSON object; nothing was written. Add these hooks by hand:\n" + JSON.stringify({ hooks: entries }, null, 2) + "\nand these keys for the accord panel:\n" + keys);
27966
+ }
27967
+ if (JSON.stringify(merged) === JSON.stringify(parsed)) return { status: "unchanged" };
27968
+ return {
27969
+ status: "updated",
27970
+ text: JSON.stringify(merged, null, 2) + "\n"
27971
+ };
27972
+ }
27973
+ /** The write half: applies a plan that is already known to be valid. */
27974
+ function writeHooks(root, plan, out) {
27975
+ if (plan === void 0) return;
27976
+ if (plan.text !== void 0) {
27977
+ const file = join(root, ...SETTINGS.split("/"));
27978
+ mkdirSync(dirname(file), { recursive: true });
27979
+ writeFileSync(file, plan.text);
27980
+ }
27981
+ out.push({
27982
+ status: plan.status,
27983
+ path: SETTINGS
27984
+ });
27985
+ }
27986
+ /**
27987
+ * D-113: the scan opens every directory the table can produce (`allSkillDirs()`), and an accord
27988
+ * directory is an orphan when this render does not produce it: a dropped runtime, a role no longer
27989
+ * declared, or a 0.1.0 `accord-ba` or `accord-dev` that the stage skills replaced while `roles:` still
27990
+ * names them. The declared subset cannot be the scan set: a directory abandoned by dropping a runtime is
27991
+ * by definition not in it, so it would never be opened at all. `sync` never deletes — an orphan keeps
27992
+ * loading in the runtime until someone removes it, so saying nothing was rejected, and deleting files
27993
+ * under a user's repository was rejected harder.
27994
+ *
27995
+ * D-123: `accord` and `accord-*` and nothing else. A directory another tool owns is not read, not
27996
+ * counted, and not mentioned — inspecting someone else's files and editorialising about them is outside
27997
+ * accord's boundary.
27998
+ */
27999
+ function orphans(root, config) {
28000
+ const rendered = new Set(skillTargets(config).map((t) => t.path.slice(0, t.path.lastIndexOf("/"))));
28001
+ const found = [];
28002
+ for (const dir of allSkillDirs()) {
28003
+ const abs = join(root, ...dir.split("/"));
28004
+ if (!scannable(root, dir)) continue;
28005
+ for (const entry of readdirSync(abs, { withFileTypes: true })) {
28006
+ if (!entry.isDirectory() || !(entry.name === "accord" || entry.name.startsWith("accord-"))) continue;
28007
+ if (!rendered.has(dir + "/" + entry.name)) found.push(dir + "/" + entry.name);
28008
+ }
28009
+ }
28010
+ if (scannable(root, ".claude/agents")) {
28011
+ const rendered = new Set(agentTargets(config).map((t) => t.path));
28012
+ for (const entry of readdirSync(join(root, ...agentDir.split("/")), { withFileTypes: true })) {
28013
+ const path = agentDir + "/" + entry.name;
28014
+ if (entry.isFile() && entry.name.startsWith("accord-") && entry.name.endsWith(".md") && !rendered.has(path)) found.push(path);
28015
+ }
28016
+ }
28017
+ if (scannable(root, ".claude/accord-panel") && pluginTargets(config, package_default).length === 0) found.push(pluginDir);
28018
+ return found.sort();
28019
+ }
28020
+ /**
28021
+ * The read side of T-06-03: every component of `dir` under `root` must exist AND be a real directory
28022
+ * before the scan opens it. The single stat call this replaced followed links, so a junction at an
28023
+ * UNDECLARED target directory — one the write pre-pass never visits — routed the scan outside the
28024
+ * repository and printed an `rm -rf` aimed through it.
28025
+ *
28026
+ * Deliberately not merged with `assertNoLink` in `../guard.js`, which walks the same way. That one answers
28027
+ * "may accord WRITE here", and a link is a refusal: exit 2, nothing written. This one answers "may accord
28028
+ * LOOK here", and a link is a skip, because D-113 fixes that the orphan report must not change the exit code
28029
+ * and D-123 forbids reporting a directory accord did not generate. Same walk, different questions,
28030
+ * different dispositions.
28031
+ *
28032
+ * False also covers "does not exist" — a first run has neither directory — which is the union the one
28033
+ * link-following call answered before.
28034
+ */
28035
+ function scannable(root, dir) {
28036
+ const parts = dir.split("/");
28037
+ for (let i = 0; i < parts.length; i++) if (lstatSync(join(root, ...parts.slice(0, i + 1)), { throwIfNoEntry: false })?.isDirectory() !== true) return false;
28038
+ return true;
28039
+ }
28040
+ function skills(ctx) {
28041
+ const config = ctx.snapshot.config;
28042
+ if (config === void 0) throw new UsageError("accord/config.yml is missing or failed its schema; run accord lint");
28043
+ const targets = [
28044
+ ...skillTargets(config),
28045
+ ...agentTargets(config),
28046
+ ...pluginTargets(config, package_default)
28047
+ ];
28048
+ for (const { path } of targets) assertNoLink(ctx.root, path);
28049
+ const hooks = planHooks(ctx.root, config);
28050
+ const results = writeSkillFiles(ctx.root, targets);
28051
+ writeHooks(ctx.root, hooks, results);
28052
+ const stale = orphans(ctx.root, config);
28053
+ ctx.stdout.write(results.map((r) => r.status + " " + r.path + "\n").join(""));
28054
+ ctx.stderr.write(stale.map((d) => "orphan " + d + " - no longer declared; accord never deletes - remove it with: rm -rf " + d + (d === ".claude/accord-panel" ? " - then remove extraKnownMarketplaces.accord and enabledPlugins[\"accord-panel@accord\"] from " + SETTINGS : "") + "\n").join(""));
28055
+ return 0;
28056
+ }
28057
+ //#endregion
28058
+ //#region src/commands/branch-fact.ts
28059
+ function branchFactFor(ctx, id) {
28060
+ const config = ctx.snapshot.config;
28061
+ const scope = Object.hasOwn(ctx.snapshot.tickets, id) && ctx.snapshot.tickets[id].frontmatter?.lane === "S";
28062
+ if (config === void 0) return gitBranchFact(ctx.root, id, {
28063
+ rendered: [],
28064
+ scope
28065
+ });
28066
+ return gitBranchFact(ctx.root, id, {
28067
+ rendered: [
28068
+ ...skillTargets(config),
28069
+ ...agentTargets(config),
28070
+ ...pluginTargets(config, package_default)
28071
+ ],
28072
+ scope,
28073
+ settings: {
28074
+ path: SETTINGS,
28075
+ expected: (base) => {
28076
+ try {
28077
+ const plan = hooksPlanFor(base, config);
28078
+ return plan === void 0 ? void 0 : plan.status === "unchanged" ? base : plan.text;
28079
+ } catch {
28080
+ return;
28081
+ }
28082
+ }
28083
+ }
28084
+ });
28085
+ }
28086
+ //#endregion
28087
+ //#region src/commands/gate.ts
28088
+ function gate(ctx, which, id, options) {
28089
+ const withCommits = which === "done" && ctx.snapshot.config?.commits?.trailers === "forbid" && ctx.snapshot.git !== void 0 ? {
28090
+ ...ctx.snapshot,
28091
+ commits: gitBranchCommits(ctx.root)
28092
+ } : ctx.snapshot;
28093
+ const forDone = !(which === "done" && Object.hasOwn(ctx.snapshot.tickets, id) && ctx.snapshot.tickets[id].frontmatter?.lane === "S") ? withCommits : (() => {
28094
+ const after = Object.fromEntries(Object.entries(ctx.snapshot.afterVerify ?? {}).filter(([k]) => k !== id));
28095
+ const found = gitAfterReview(ctx.root, id);
28096
+ if (found !== void 0) after[id] = found;
28097
+ return {
28098
+ ...withCommits,
28099
+ reviewFacts: { [id]: gitReviewFact(ctx.root, id) },
28100
+ afterVerify: after
28101
+ };
28102
+ })();
28103
+ const forScope = which === "done" && Object.hasOwn(ctx.snapshot.tickets, id) && ctx.snapshot.tickets[id].frontmatter?.lane !== void 0 ? {
28104
+ ...forDone,
28105
+ branchFacts: { [id]: branchFactFor(ctx, id) }
28106
+ } : forDone;
28107
+ const result = which === "ready" ? gateReady(ctx.snapshot, id) : which === "plan" ? gatePlan(ctx.snapshot, id) : gateDone(forScope, id);
28108
+ ctx.stdout.write(options.json ? JSON.stringify(result, null, 2) + "\n" : colourFindings(renderText(result), ctx.stdout));
28109
+ if (which === "ready" && result.verdict === "pass" && result.acHash !== void 0) {
28110
+ const rel = ctx.snapshot.tickets[id].file;
28111
+ try {
28112
+ if (writeKey(ctx.root, rel, "ac_hash", result.acHash)) {
28113
+ const prev = ctx.snapshot.tickets[id].frontmatter?.ac_hash;
28114
+ ctx.stderr.write(prev === void 0 ? "wrote ac_hash " + result.acHash + " to " + rel + "\n" : "updated ac_hash " + prev + " -> " + result.acHash + " in " + rel + "\n");
28115
+ }
28116
+ } catch (err) {
28117
+ throw new UsageError("cannot write ac_hash to " + rel + ": " + (err instanceof Error ? err.message : String(err)));
28118
+ }
28119
+ }
28120
+ if (which === "plan" && result.verdict === "pass" && result.planHash !== void 0) {
28121
+ const rel = ctx.snapshot.work?.[id]?.plan?.file;
28122
+ if (rel !== void 0) try {
28123
+ if (writeKey(ctx.root, rel, "plan_hash", result.planHash)) ctx.stderr.write("wrote plan_hash " + result.planHash + " to " + rel + "\n");
28124
+ } catch (err) {
28125
+ throw new UsageError("cannot write plan_hash to " + rel + ": " + (err instanceof Error ? err.message : String(err)));
28126
+ }
28127
+ }
28128
+ return result.verdict === "fail" ? 1 : 0;
28129
+ }
28130
+ //#endregion
28131
+ //#region src/load/proc.ts
28132
+ const WIN_SCRIPT = "[Console]::OutputEncoding = [Text.Encoding]::UTF8; $r = @(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; ppid = [int]$_.ParentProcessId; start = $(if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') } else { '' }) } }); ConvertTo-Json -InputObject $r -Compress";
28133
+ /** The OS process table. A failure throws: a hold is never judged on a guess. */
28134
+ function processTable() {
27346
28135
  const opts = {
27347
28136
  encoding: "utf8",
27348
28137
  stdio: [
@@ -28785,7 +29574,7 @@ function hook(ctx, event) {
28785
29574
  const root = input?.cwd !== void 0 ? repoRoot(input.cwd) : ctx.root;
28786
29575
  const marker = input?.session_id ?? ctx.env.CLAUDE_CODE_SESSION_ID;
28787
29576
  const held = marker && !isLinked(root) ? heldEntry(root, marker, ctx) : void 0;
28788
- if (held !== void 0) body = (anchorText(ctx.snapshot, held.id) ?? "") + `this session works ${held.id}: call EnterWorktree with the path on the next line, then run accord anchor there and go on at the stage it prints\nenter: ${held.path}\n`;
29577
+ if (held !== void 0) body = `this session works ${held.id}: call EnterWorktree with the path on the next line, then run accord anchor there and go on at the stage it prints\nenter: ${held.path}\n`;
28789
29578
  else {
28790
29579
  const id = activeTicket(root, ctx.snapshot);
28791
29580
  body = (id === void 0 ? void 0 : anchorText(ctx.snapshot, id)) ?? body;
@@ -28904,207 +29693,32 @@ function promptExpansion(ctx) {
28904
29693
  }
28905
29694
  }
28906
29695
  //#endregion
28907
- //#region src/guard.ts
28908
- /**
28909
- * Every component of `path` under `root` must be a real directory, and its leaf a regular file — never a
28910
- * symlink or a Windows junction. `lstat` declines to follow only the FINAL component, so checking the leaf
28911
- * alone leaves a junction one directory up (`.claude/skills/accord-designer`) routing every write inside it
28912
- * to wherever the link points, with the guard never firing.
28913
- *
28914
- * The first component that does not exist ends the walk: `mkdirSync(..., { recursive: true })` creates real
28915
- * directories, so nothing below it can be a link.
28916
- *
28917
- * Deliberately not merged with `scannable` in `commands/skills.ts`, which walks the same way. This one
28918
- * answers "may accord WRITE here", and a link is a refusal: exit 2, nothing written. That one answers "may
28919
- * accord LOOK here", and a link is a silent skip, because D-113 fixes that the orphan report must not change
28920
- * the exit code. Same walk, different questions, different dispositions.
28921
- */
28922
- function assertNoLink(root, path) {
28923
- const parts = path.split("/");
28924
- for (let i = 0; i < parts.length; i++) {
28925
- const found = lstatSync(join(root, ...parts.slice(0, i + 1)), { throwIfNoEntry: false });
28926
- if (found === void 0) return;
28927
- const leaf = i === parts.length - 1;
28928
- if (leaf ? !found.isFile() : !found.isDirectory()) throw new UsageError(parts.slice(0, i + 1).join("/") + " is not a regular " + (leaf ? "file" : "directory") + " - nothing was written");
28929
- }
28930
- }
28931
- //#endregion
28932
29696
  //#region src/pin.ts
28933
29697
  /**
28934
29698
  * The refusal message when the pin and the running version differ, else undefined.
28935
29699
  *
28936
29700
  * `running` is a parameter rather than a read of `pkg.version` so a test can drive a mismatch without
28937
29701
  * rewriting package.json. The message names both versions and the command that fixes it (CLI-06).
29702
+ * `onDisk` is the version in the package.json beside the build: when it differs from `running`, the
29703
+ * build is stale (a clone of accord after a release), and npx would only run that same build (BUG-LN-10).
28938
29704
  */
28939
- function pinMessage(pinned, running) {
29705
+ function pinMessage(pinned, running, onDisk) {
28940
29706
  if (pinned === void 0 || pinned === running) return void 0;
29707
+ if (onDisk !== void 0 && onDisk !== running) return `config.yml pins accord ${pinned}, running ${running} - this build is stale, its package.json says ${onDisk} - run: npm run build`;
28941
29708
  return `config.yml pins accord ${pinned}, running ${running} - run: npx --yes ${package_default.name}@${pinned}`;
28942
29709
  }
28943
- //#endregion
28944
- //#region src/commands/skills.ts
28945
29710
  /**
28946
- * D-131: the one place accord writes a skill copy. `sync` and `init` both call it, so the marker-and-hash
28947
- * rule above cannot come to mean one thing under one command and something else under the other.
28948
- *
28949
- * The caller owns everything around it — the `config.yml` requirement, the `assertNoLink` pre-pass, the
28950
- * orphan scan, and the report — because those differ between the two callers and this does not.
28951
- *
28952
- * GC-WR-01: the caller may own the array too, so a status is recorded at the moment it becomes true rather
28953
- * than when the loop finishes — a run that throws at target two still reports the `overwrote local edits`
28954
- * that really happened at target one, which is the whole of what D-112 promises. The cost is that the
28955
- * declared row type is wider than anything this function can produce: the element type belongs to the
28956
- * report `init` prints, not to this function's own four states.
29711
+ * The version in the package.json beside this module, read at run time, or undefined when it cannot be
29712
+ * read. The bundle bakes `pkg.version` in at build time, so the two differ only when the source moved on
29713
+ * after the last build; `../package.json` resolves the same from `src/` and from `dist/`.
28957
29714
  */
28958
- function writeSkillFiles(root, targets, out = []) {
28959
- for (const { path, text } of targets) {
28960
- const file = join(root, ...path.split("/"));
28961
- if (lstatSync(file, { throwIfNoEntry: false }) === void 0) {
28962
- mkdirSync(dirname(file), { recursive: true });
28963
- writeFileSync(file, text);
28964
- out.push({
28965
- status: "created",
28966
- path
28967
- });
28968
- continue;
28969
- }
28970
- const onDisk = normaliseText(readFileSync(file, "utf8"));
28971
- const stored = markerHash(onDisk);
28972
- const intact = stored !== void 0 && stored === contentHash(withoutMarker(onDisk));
28973
- if (intact && onDisk === text) {
28974
- out.push({
28975
- status: "unchanged",
28976
- path
28977
- });
28978
- continue;
28979
- }
28980
- writeFileSync(file, text);
28981
- out.push({
28982
- status: intact ? "updated" : "overwrote local edits",
28983
- path
28984
- });
28985
- }
28986
- return out;
28987
- }
28988
- const SETTINGS = ".claude/settings.json";
28989
- /**
28990
- * The pre-pass half: the link guard, then read, parse and merge `.claude/settings.json` for the claude runtime
28991
- * only. The file is the owner's: anything that is not a JSON object is refused here, with the hooks printed to
28992
- * add by hand, so the refusal lands before the first byte of the run. Writes nothing.
28993
- */
28994
- function planHooks(root, config) {
28995
- if (!config.runtimes.includes("claude")) return void 0;
28996
- assertNoLink(root, SETTINGS);
28997
- const file = join(root, ...SETTINGS.split("/"));
28998
- return hooksPlanFor(lstatSync(file, { throwIfNoEntry: false }) === void 0 ? void 0 : readFileSync(file, "utf8"), config);
28999
- }
29000
- /** ST-LN-25: the plan for a settings file whose body is `text` (undefined: it does not exist); `planHooks` minus the file read. */
29001
- function hooksPlanFor(text, config) {
29002
- if (!config.runtimes.includes("claude")) return void 0;
29003
- const entries = hookEntries(package_default);
29004
- if (text === void 0) return {
29005
- status: "created",
29006
- text: JSON.stringify(mergePanel(mergeHooks({}, entries)), null, 2) + "\n"
29007
- };
29008
- let parsed;
29009
- let merged;
29715
+ function versionOnDisk() {
29010
29716
  try {
29011
- parsed = JSON.parse(text);
29012
- merged = mergePanel(mergeHooks(parsed, entries));
29013
- } catch (err) {
29014
- const keys = JSON.stringify(panelSettings, null, 2);
29015
- if (err instanceof RangeError) throw new UsageError(SETTINGS + " already declares a marketplace named accord with another source; nothing was written. Rename it, then add these keys for the accord panel by hand:\n" + keys);
29016
- if (!(err instanceof SyntaxError || err instanceof TypeError)) throw err;
29017
- throw new UsageError(SETTINGS + " is not a JSON object; nothing was written. Add these hooks by hand:\n" + JSON.stringify({ hooks: entries }, null, 2) + "\nand these keys for the accord panel:\n" + keys);
29018
- }
29019
- if (JSON.stringify(merged) === JSON.stringify(parsed)) return { status: "unchanged" };
29020
- return {
29021
- status: "updated",
29022
- text: JSON.stringify(merged, null, 2) + "\n"
29023
- };
29024
- }
29025
- /** The write half: applies a plan that is already known to be valid. */
29026
- function writeHooks(root, plan, out) {
29027
- if (plan === void 0) return;
29028
- if (plan.text !== void 0) {
29029
- const file = join(root, ...SETTINGS.split("/"));
29030
- mkdirSync(dirname(file), { recursive: true });
29031
- writeFileSync(file, plan.text);
29032
- }
29033
- out.push({
29034
- status: plan.status,
29035
- path: SETTINGS
29036
- });
29037
- }
29038
- /**
29039
- * D-113: the scan opens every directory the table can produce (`allSkillDirs()`), and an accord
29040
- * directory is an orphan when this render does not produce it: a dropped runtime, a role no longer
29041
- * declared, or a 0.1.0 `accord-ba` or `accord-dev` that the stage skills replaced while `roles:` still
29042
- * names them. The declared subset cannot be the scan set: a directory abandoned by dropping a runtime is
29043
- * by definition not in it, so it would never be opened at all. `sync` never deletes — an orphan keeps
29044
- * loading in the runtime until someone removes it, so saying nothing was rejected, and deleting files
29045
- * under a user's repository was rejected harder.
29046
- *
29047
- * D-123: `accord` and `accord-*` and nothing else. A directory another tool owns is not read, not
29048
- * counted, and not mentioned — inspecting someone else's files and editorialising about them is outside
29049
- * accord's boundary.
29050
- */
29051
- function orphans(root, config) {
29052
- const rendered = new Set(skillTargets(config).map((t) => t.path.slice(0, t.path.lastIndexOf("/"))));
29053
- const found = [];
29054
- for (const dir of allSkillDirs()) {
29055
- const abs = join(root, ...dir.split("/"));
29056
- if (!scannable(root, dir)) continue;
29057
- for (const entry of readdirSync(abs, { withFileTypes: true })) {
29058
- if (!entry.isDirectory() || !(entry.name === "accord" || entry.name.startsWith("accord-"))) continue;
29059
- if (!rendered.has(dir + "/" + entry.name)) found.push(dir + "/" + entry.name);
29060
- }
29061
- }
29062
- if (scannable(root, ".claude/agents")) {
29063
- const rendered = new Set(agentTargets(config).map((t) => t.path));
29064
- for (const entry of readdirSync(join(root, ...agentDir.split("/")), { withFileTypes: true })) {
29065
- const path = agentDir + "/" + entry.name;
29066
- if (entry.isFile() && entry.name.startsWith("accord-") && entry.name.endsWith(".md") && !rendered.has(path)) found.push(path);
29067
- }
29717
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
29718
+ return typeof version === "string" ? version : void 0;
29719
+ } catch {
29720
+ return;
29068
29721
  }
29069
- if (scannable(root, ".claude/accord-panel") && pluginTargets(config, package_default).length === 0) found.push(pluginDir);
29070
- return found.sort();
29071
- }
29072
- /**
29073
- * The read side of T-06-03: every component of `dir` under `root` must exist AND be a real directory
29074
- * before the scan opens it. The single stat call this replaced followed links, so a junction at an
29075
- * UNDECLARED target directory — one the write pre-pass never visits — routed the scan outside the
29076
- * repository and printed an `rm -rf` aimed through it.
29077
- *
29078
- * Deliberately not merged with `assertNoLink` in `../guard.js`, which walks the same way. That one answers
29079
- * "may accord WRITE here", and a link is a refusal: exit 2, nothing written. This one answers "may accord
29080
- * LOOK here", and a link is a skip, because D-113 fixes that the orphan report must not change the exit code
29081
- * and D-123 forbids reporting a directory accord did not generate. Same walk, different questions,
29082
- * different dispositions.
29083
- *
29084
- * False also covers "does not exist" — a first run has neither directory — which is the union the one
29085
- * link-following call answered before.
29086
- */
29087
- function scannable(root, dir) {
29088
- const parts = dir.split("/");
29089
- for (let i = 0; i < parts.length; i++) if (lstatSync(join(root, ...parts.slice(0, i + 1)), { throwIfNoEntry: false })?.isDirectory() !== true) return false;
29090
- return true;
29091
- }
29092
- function skills(ctx) {
29093
- const config = ctx.snapshot.config;
29094
- if (config === void 0) throw new UsageError("accord/config.yml is missing or failed its schema; run accord lint");
29095
- const targets = [
29096
- ...skillTargets(config),
29097
- ...agentTargets(config),
29098
- ...pluginTargets(config, package_default)
29099
- ];
29100
- for (const { path } of targets) assertNoLink(ctx.root, path);
29101
- const hooks = planHooks(ctx.root, config);
29102
- const results = writeSkillFiles(ctx.root, targets);
29103
- writeHooks(ctx.root, hooks, results);
29104
- const stale = orphans(ctx.root, config);
29105
- ctx.stdout.write(results.map((r) => r.status + " " + r.path + "\n").join(""));
29106
- ctx.stderr.write(stale.map((d) => "orphan " + d + " - no longer declared; accord never deletes - remove it with: rm -rf " + d + (d === ".claude/accord-panel" ? " - then remove extraKnownMarketplaces.accord and enabledPlugins[\"accord-panel@accord\"] from " + SETTINGS : "") + "\n").join(""));
29107
- return 0;
29108
29722
  }
29109
29723
  //#endregion
29110
29724
  //#region src/commands/init.ts
@@ -29127,7 +29741,7 @@ function init(ctx) {
29127
29741
  const refusals = (extra) => {
29128
29742
  const snapshot = loadSnapshot(loadFromFs(ctx.root));
29129
29743
  if (snapshot.config === void 0) throw new UsageError("accord/config.yml is missing or failed its schema; run accord lint");
29130
- const message = pinMessage(snapshot.config.accord, package_default.version);
29744
+ const message = pinMessage(snapshot.config.accord, package_default.version, versionOnDisk());
29131
29745
  if (message !== void 0) throw new UsageError(message);
29132
29746
  const targets = [...skillTargets(snapshot.config), ...extra ?? [...agentTargets(snapshot.config), ...pluginTargets(snapshot.config, package_default)]];
29133
29747
  for (const { path } of targets) assertNoLink(ctx.root, path);
@@ -29502,6 +30116,20 @@ function reviewPackage(ctx, id, options) {
29502
30116
  } catch (e) {
29503
30117
  throw new UsageError(`cannot diff ${range}: ${why(e)}`);
29504
30118
  }
30119
+ const f = branchFactFor(ctx, id);
30120
+ const s = {
30121
+ ...ctx.snapshot,
30122
+ branchFacts: { [id]: f }
30123
+ };
30124
+ const cannot = (why) => `(cannot tell: ${why})`;
30125
+ const shown = [];
30126
+ if (laneS) {
30127
+ const o = outsideScope(s, id);
30128
+ shown.push("## Out-of-scope files", "error" in f ? cannot(f.error) : o !== void 0 && "cannot" in o ? cannot(o.cannot) : o?.paths.join("\n") || "(none)");
30129
+ }
30130
+ const signs = interfaceSigns(s, id);
30131
+ shown.push("## Interface signs", "error" in f ? cannot(f.error) : signs?.map((x) => `${x.path}: ${x.sign}`).join("\n") || "(none)");
30132
+ parts.splice(parts.indexOf("## Stat"), 0, ...shown);
29505
30133
  ctx.stdout.write(parts.join("\n\n") + "\n");
29506
30134
  return 0;
29507
30135
  }
@@ -29991,10 +30619,17 @@ function upgrade(ctx) {
29991
30619
  text: next
29992
30620
  }];
29993
30621
  const workflow = files[WORKFLOW];
30622
+ const refresh = workflow === void 0 ? void 0 : refreshLintStep(workflow, package_default.name, v);
29994
30623
  if (workflow !== void 0) writes.push({
29995
30624
  path: WORKFLOW,
29996
- text: repinWorkflow(workflow, package_default.name, pinned, v)
30625
+ text: repinWorkflow(refresh !== void 0 && "text" in refresh ? refresh.text : workflow, package_default.name, pinned, v)
29997
30626
  });
30627
+ let warning;
30628
+ if (refresh !== void 0 && "refused" in refresh) {
30629
+ const rerun = `run accord upgrade again before merging upgrade/${v} (close its pull request, delete the branch with git push origin --delete upgrade/${v}, and fix the step on ${short} first); otherwise the refresh waits for the next release`;
30630
+ const fix = refresh.refused === "count" ? "leave exactly one step named lint and gate and" : "the step named lint and gate must be a block list item without an anchor, as accord init writes it; make it one and";
30631
+ warning = `warning: the lint-and-gate script in ${WORKFLOW} was not refreshed: ${fix} ${rerun}`;
30632
+ }
29998
30633
  const hooks = hooksPlanFor(files[SETTINGS], config);
29999
30634
  if (hooks?.text !== void 0) writes.push({
30000
30635
  path: SETTINGS,
@@ -30042,9 +30677,10 @@ function upgrade(ctx) {
30042
30677
  sha + ":refs/heads/upgrade/" + v
30043
30678
  ]);
30044
30679
  } catch (err) {
30045
- throw new UsageError(`the upgrade commit ${sha} was made but the push failed: ${String(err.stderr ?? err).trim().split("\n")[0]} - push it with git push origin ${sha}:refs/heads/upgrade/${v}`);
30680
+ throw new UsageError(`the upgrade commit ${sha} was made but the push failed: ${String(err.stderr ?? err).trim().split("\n")[0]} - push it with git push origin ${sha}:refs/heads/upgrade/${v}` + (warning === void 0 ? "" : "\n" + warning));
30046
30681
  }
30047
30682
  ctx.stdout.write(`branch: upgrade/${v}\nbase: ${short}\ntitle: upgrade: accord ${v}\n`);
30683
+ if (warning !== void 0) ctx.stderr.write(warning + "\n");
30048
30684
  return 0;
30049
30685
  }
30050
30686
  //#endregion
@@ -30062,7 +30698,7 @@ function preflight(opts, mode = "checkout") {
30062
30698
  }));
30063
30699
  const plain = parts === void 0 ? void 0 : parts.checkout === parts.view ? snapshot : loadSnapshot(parts.checkout);
30064
30700
  if (snapshot.config !== void 0) {
30065
- const message = pinMessage(snapshot.config.accord, package_default.version);
30701
+ const message = pinMessage(snapshot.config.accord, package_default.version, versionOnDisk());
30066
30702
  if (message !== void 0) throw new UsageError(message);
30067
30703
  }
30068
30704
  return {
@@ -30177,7 +30813,7 @@ function buildProgram(opts, setCode) {
30177
30813
  program.command("next").description("list lane tickets that passed Ready, have no task recorded, and are unblocked and unclaimed").option("--json", "print the StatusRow array instead of the table").action((options) => {
30178
30814
  setCode(next(preflight(opts, "stage"), options));
30179
30815
  });
30180
- program.command("dashboard").description("print the ranked and capped sections for the viewer").option("--json", "print the Dashboard object instead of text").action(async (options) => {
30816
+ program.command("dashboard").description("print the ranked sections for the viewer").option("--json", "print the Dashboard object instead of text").action(async (options) => {
30181
30817
  setCode(await dashboardCommand(preflight(opts, "claims"), options));
30182
30818
  });
30183
30819
  program.command("anchor [id]").description("print the active ticket's intent, vetoes, decisions, corrections, and position").action((id) => {