@event4u/agent-config 9.34.0 → 9.35.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 (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +91 -0
  3. package/CONTRIBUTING.md +23 -14
  4. package/dist/agent-src/commands/roadmap/next.md +37 -5
  5. package/dist/agent-src/scripts/roadmap_gates.ts +119 -4
  6. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +25 -0
  7. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  8. package/dist/agent-src/templates/scripts/memory_lookup.ts +1 -2
  9. package/dist/agent-src/templates/scripts/memory_signal.ts +2 -4
  10. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  11. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  12. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  13. package/dist/cli/registry.js +2 -2
  14. package/dist/cli/registry.js.map +1 -1
  15. package/dist/cli-delegate/{chunk-REMEZSVP.js → chunk-2TGMGLBL.js} +2 -2
  16. package/dist/cli-delegate/{chunk-YPAWXXXR.js → chunk-55RI5UXK.js} +17 -0
  17. package/dist/cli-delegate/{chunk-MA4GFWDY.js → chunk-H6F2CQFI.js} +1 -1
  18. package/dist/cli-delegate/{chunk-UVKNIJH2.js → chunk-ZAXH3ZWU.js} +3 -0
  19. package/dist/cli-delegate/cmd_conformance.js +2 -2
  20. package/dist/cli-delegate/cmd_doctor.js +2 -2
  21. package/dist/cli-delegate/cmd_export.js +2 -2
  22. package/dist/cli-delegate/cmd_handoff.js +1 -1
  23. package/dist/cli-delegate/cmd_prune.js +2 -2
  24. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  25. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  26. package/dist/cli-delegate/cmd_sync.js +2 -2
  27. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  28. package/dist/cli-delegate/cmd_validate.js +2 -2
  29. package/dist/discovery/deprecation-report.md +1 -1
  30. package/dist/discovery/discovery-manifest.json +4 -4
  31. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  32. package/dist/discovery/discovery-manifest.summary.md +1 -1
  33. package/dist/discovery/dormancy-report.md +1 -1
  34. package/dist/discovery/orphan-report.md +1 -1
  35. package/dist/discovery/packs.json +2 -2
  36. package/dist/discovery/trust-report.md +1 -1
  37. package/dist/discovery/workspaces.json +2 -2
  38. package/dist/hooks/dispatch.js +259 -78
  39. package/dist/install/install.mjs +3 -0
  40. package/dist/mcp/registry-manifest.json +2 -2
  41. package/dist/mcp/server.json +2 -2
  42. package/dist/mcp/server.mjs +74 -2
  43. package/dist/server/schemas/settings.js +1 -0
  44. package/dist/server/schemas/settings.js.map +1 -1
  45. package/docs/contracts/settings-classes.md +3 -2
  46. package/package.json +1 -1
  47. package/src/agent-src/scripts/roadmap_gates.ts +119 -4
  48. package/src/agent-src/templates/scripts/memory_lookup.ts +1 -2
  49. package/src/agent-src/templates/scripts/memory_signal.ts +2 -4
  50. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +35 -5
  51. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation.ts +252 -0
  52. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.ts +307 -0
  53. package/src/config/agent-settings.template.yml +11 -2
  54. package/src/config/quorum-attendance-budget.json +10 -2
  55. package/src/scripts/_dispatch.bash +6 -2
  56. package/src/scripts/_lib/session_register.ts +67 -1
  57. package/src/scripts/ai_council/config.ts +60 -0
  58. package/src/scripts/ai_council/events_log.ts +103 -4
  59. package/src/scripts/ai_council/quorum.ts +83 -6
  60. package/src/scripts/council_cli.ts +36 -1
  61. package/src/scripts/hooks/turn_end_gate_hook.ts +172 -11
  62. package/src/scripts/lint_framework_leakage.ts +195 -28
  63. package/src/scripts/lint_framework_leakage_allowlist.json +94 -86
  64. package/src/scripts/memory_lookup.ts +1 -2
  65. package/src/scripts/schemas/command.schema.json +4 -0
  66. package/src/scripts/schemas/skill.schema.json +4 -0
  67. package/src/scripts/session_register_hook.ts +238 -18
  68. package/src/scripts/sessions_cli.ts +377 -12
  69. package/src/server/schemas/settings.ts +3 -0
@@ -3996,10 +3996,10 @@ var require_resolve_block_map = __commonJS({
3996
3996
  let offset = bm.offset;
3997
3997
  let commentEnd = null;
3998
3998
  for (const collItem of bm.items) {
3999
- const { start, key, sep: sep6, value } = collItem;
3999
+ const { start, key, sep: sep7, value } = collItem;
4000
4000
  const keyProps = resolveProps.resolveProps(start, {
4001
4001
  indicator: "explicit-key-ind",
4002
- next: key ?? sep6?.[0],
4002
+ next: key ?? sep7?.[0],
4003
4003
  offset,
4004
4004
  onError,
4005
4005
  parentIndent: bm.indent,
@@ -4013,7 +4013,7 @@ var require_resolve_block_map = __commonJS({
4013
4013
  else if ("indent" in key && key.indent !== bm.indent)
4014
4014
  onError(offset, "BAD_INDENT", startColMsg);
4015
4015
  }
4016
- if (!keyProps.anchor && !keyProps.tag && !sep6) {
4016
+ if (!keyProps.anchor && !keyProps.tag && !sep7) {
4017
4017
  commentEnd = keyProps.end;
4018
4018
  if (keyProps.comment) {
4019
4019
  if (map.comment)
@@ -4037,7 +4037,7 @@ var require_resolve_block_map = __commonJS({
4037
4037
  ctx.atKey = false;
4038
4038
  if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
4039
4039
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
4040
- const valueProps = resolveProps.resolveProps(sep6 ?? [], {
4040
+ const valueProps = resolveProps.resolveProps(sep7 ?? [], {
4041
4041
  indicator: "map-value-ind",
4042
4042
  next: value,
4043
4043
  offset: keyNode.range[2],
@@ -4053,7 +4053,7 @@ var require_resolve_block_map = __commonJS({
4053
4053
  if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
4054
4054
  onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
4055
4055
  }
4056
- const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep6, null, valueProps, onError);
4056
+ const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep7, null, valueProps, onError);
4057
4057
  if (ctx.schema.compat)
4058
4058
  utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
4059
4059
  offset = valueNode.range[2];
@@ -4144,7 +4144,7 @@ var require_resolve_end = __commonJS({
4144
4144
  let comment = "";
4145
4145
  if (end) {
4146
4146
  let hasSpace = false;
4147
- let sep6 = "";
4147
+ let sep7 = "";
4148
4148
  for (const token of end) {
4149
4149
  const { source, type } = token;
4150
4150
  switch (type) {
@@ -4158,13 +4158,13 @@ var require_resolve_end = __commonJS({
4158
4158
  if (!comment)
4159
4159
  comment = cb;
4160
4160
  else
4161
- comment += sep6 + cb;
4162
- sep6 = "";
4161
+ comment += sep7 + cb;
4162
+ sep7 = "";
4163
4163
  break;
4164
4164
  }
4165
4165
  case "newline":
4166
4166
  if (comment)
4167
- sep6 += source;
4167
+ sep7 += source;
4168
4168
  hasSpace = true;
4169
4169
  break;
4170
4170
  default:
@@ -4207,18 +4207,18 @@ var require_resolve_flow_collection = __commonJS({
4207
4207
  let offset = fc.offset + fc.start.source.length;
4208
4208
  for (let i = 0; i < fc.items.length; ++i) {
4209
4209
  const collItem = fc.items[i];
4210
- const { start, key, sep: sep6, value } = collItem;
4210
+ const { start, key, sep: sep7, value } = collItem;
4211
4211
  const props = resolveProps.resolveProps(start, {
4212
4212
  flow: fcName,
4213
4213
  indicator: "explicit-key-ind",
4214
- next: key ?? sep6?.[0],
4214
+ next: key ?? sep7?.[0],
4215
4215
  offset,
4216
4216
  onError,
4217
4217
  parentIndent: fc.indent,
4218
4218
  startOnNewline: false
4219
4219
  });
4220
4220
  if (!props.found) {
4221
- if (!props.anchor && !props.tag && !sep6 && !value) {
4221
+ if (!props.anchor && !props.tag && !sep7 && !value) {
4222
4222
  if (i === 0 && props.comma)
4223
4223
  onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
4224
4224
  else if (i < fc.items.length - 1)
@@ -4272,8 +4272,8 @@ var require_resolve_flow_collection = __commonJS({
4272
4272
  }
4273
4273
  }
4274
4274
  }
4275
- if (!isMap && !sep6 && !props.found) {
4276
- const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep6, null, props, onError);
4275
+ if (!isMap && !sep7 && !props.found) {
4276
+ const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep7, null, props, onError);
4277
4277
  coll.items.push(valueNode);
4278
4278
  offset = valueNode.range[2];
4279
4279
  if (isBlock(value))
@@ -4285,7 +4285,7 @@ var require_resolve_flow_collection = __commonJS({
4285
4285
  if (isBlock(key))
4286
4286
  onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
4287
4287
  ctx.atKey = false;
4288
- const valueProps = resolveProps.resolveProps(sep6 ?? [], {
4288
+ const valueProps = resolveProps.resolveProps(sep7 ?? [], {
4289
4289
  flow: fcName,
4290
4290
  indicator: "map-value-ind",
4291
4291
  next: value,
@@ -4296,8 +4296,8 @@ var require_resolve_flow_collection = __commonJS({
4296
4296
  });
4297
4297
  if (valueProps.found) {
4298
4298
  if (!isMap && !props.found && ctx.options.strict) {
4299
- if (sep6)
4300
- for (const st of sep6) {
4299
+ if (sep7)
4300
+ for (const st of sep7) {
4301
4301
  if (st === valueProps.found)
4302
4302
  break;
4303
4303
  if (st.type === "newline") {
@@ -4314,7 +4314,7 @@ var require_resolve_flow_collection = __commonJS({
4314
4314
  else
4315
4315
  onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
4316
4316
  }
4317
- const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep6, null, valueProps, onError) : null;
4317
+ const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep7, null, valueProps, onError) : null;
4318
4318
  if (valueNode) {
4319
4319
  if (isBlock(value))
4320
4320
  onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
@@ -4494,7 +4494,7 @@ var require_resolve_block_scalar = __commonJS({
4494
4494
  chompStart = i + 1;
4495
4495
  }
4496
4496
  let value = "";
4497
- let sep6 = "";
4497
+ let sep7 = "";
4498
4498
  let prevMoreIndented = false;
4499
4499
  for (let i = 0; i < contentStart; ++i)
4500
4500
  value += lines[i][0].slice(trimIndent) + "\n";
@@ -4511,24 +4511,24 @@ var require_resolve_block_scalar = __commonJS({
4511
4511
  indent = "";
4512
4512
  }
4513
4513
  if (type === Scalar.Scalar.BLOCK_LITERAL) {
4514
- value += sep6 + indent.slice(trimIndent) + content;
4515
- sep6 = "\n";
4514
+ value += sep7 + indent.slice(trimIndent) + content;
4515
+ sep7 = "\n";
4516
4516
  } else if (indent.length > trimIndent || content[0] === " ") {
4517
- if (sep6 === " ")
4518
- sep6 = "\n";
4519
- else if (!prevMoreIndented && sep6 === "\n")
4520
- sep6 = "\n\n";
4521
- value += sep6 + indent.slice(trimIndent) + content;
4522
- sep6 = "\n";
4517
+ if (sep7 === " ")
4518
+ sep7 = "\n";
4519
+ else if (!prevMoreIndented && sep7 === "\n")
4520
+ sep7 = "\n\n";
4521
+ value += sep7 + indent.slice(trimIndent) + content;
4522
+ sep7 = "\n";
4523
4523
  prevMoreIndented = true;
4524
4524
  } else if (content === "") {
4525
- if (sep6 === "\n")
4525
+ if (sep7 === "\n")
4526
4526
  value += "\n";
4527
4527
  else
4528
- sep6 = "\n";
4528
+ sep7 = "\n";
4529
4529
  } else {
4530
- value += sep6 + content;
4531
- sep6 = " ";
4530
+ value += sep7 + content;
4531
+ sep7 = " ";
4532
4532
  prevMoreIndented = false;
4533
4533
  }
4534
4534
  }
@@ -4710,25 +4710,25 @@ var require_resolve_flow_scalar = __commonJS({
4710
4710
  if (!match)
4711
4711
  return source;
4712
4712
  let res = match[1];
4713
- let sep6 = " ";
4713
+ let sep7 = " ";
4714
4714
  let pos = first.lastIndex;
4715
4715
  line.lastIndex = pos;
4716
4716
  while (match = line.exec(source)) {
4717
4717
  if (match[1] === "") {
4718
- if (sep6 === "\n")
4719
- res += sep6;
4718
+ if (sep7 === "\n")
4719
+ res += sep7;
4720
4720
  else
4721
- sep6 = "\n";
4721
+ sep7 = "\n";
4722
4722
  } else {
4723
- res += sep6 + match[1];
4724
- sep6 = " ";
4723
+ res += sep7 + match[1];
4724
+ sep7 = " ";
4725
4725
  }
4726
4726
  pos = line.lastIndex;
4727
4727
  }
4728
4728
  const last = /[ \t]*(.*)/sy;
4729
4729
  last.lastIndex = pos;
4730
4730
  match = last.exec(source);
4731
- return res + sep6 + (match?.[1] ?? "");
4731
+ return res + sep7 + (match?.[1] ?? "");
4732
4732
  }
4733
4733
  function doubleQuotedValue(source, onError) {
4734
4734
  let res = "";
@@ -5538,14 +5538,14 @@ var require_cst_stringify = __commonJS({
5538
5538
  }
5539
5539
  }
5540
5540
  }
5541
- function stringifyItem({ start, key, sep: sep6, value }) {
5541
+ function stringifyItem({ start, key, sep: sep7, value }) {
5542
5542
  let res = "";
5543
5543
  for (const st of start)
5544
5544
  res += st.source;
5545
5545
  if (key)
5546
5546
  res += stringifyToken(key);
5547
- if (sep6)
5548
- for (const st of sep6)
5547
+ if (sep7)
5548
+ for (const st of sep7)
5549
5549
  res += st.source;
5550
5550
  if (value)
5551
5551
  res += stringifyToken(value);
@@ -6712,18 +6712,18 @@ var require_parser = __commonJS({
6712
6712
  if (this.type === "map-value-ind") {
6713
6713
  const prev = getPrevProps(this.peek(2));
6714
6714
  const start = getFirstKeyStartProps(prev);
6715
- let sep6;
6715
+ let sep7;
6716
6716
  if (scalar.end) {
6717
- sep6 = scalar.end;
6718
- sep6.push(this.sourceToken);
6717
+ sep7 = scalar.end;
6718
+ sep7.push(this.sourceToken);
6719
6719
  delete scalar.end;
6720
6720
  } else
6721
- sep6 = [this.sourceToken];
6721
+ sep7 = [this.sourceToken];
6722
6722
  const map = {
6723
6723
  type: "block-map",
6724
6724
  offset: scalar.offset,
6725
6725
  indent: scalar.indent,
6726
- items: [{ start, key: scalar, sep: sep6 }]
6726
+ items: [{ start, key: scalar, sep: sep7 }]
6727
6727
  };
6728
6728
  this.onKeyLine = true;
6729
6729
  this.stack[this.stack.length - 1] = map;
@@ -6876,15 +6876,15 @@ var require_parser = __commonJS({
6876
6876
  } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
6877
6877
  const start2 = getFirstKeyStartProps(it.start);
6878
6878
  const key = it.key;
6879
- const sep6 = it.sep;
6880
- sep6.push(this.sourceToken);
6879
+ const sep7 = it.sep;
6880
+ sep7.push(this.sourceToken);
6881
6881
  delete it.key;
6882
6882
  delete it.sep;
6883
6883
  this.stack.push({
6884
6884
  type: "block-map",
6885
6885
  offset: this.offset,
6886
6886
  indent: this.indent,
6887
- items: [{ start: start2, key, sep: sep6 }]
6887
+ items: [{ start: start2, key, sep: sep7 }]
6888
6888
  });
6889
6889
  } else if (start.length > 0) {
6890
6890
  it.sep = it.sep.concat(start, this.sourceToken);
@@ -7078,13 +7078,13 @@ var require_parser = __commonJS({
7078
7078
  const prev = getPrevProps(parent);
7079
7079
  const start = getFirstKeyStartProps(prev);
7080
7080
  fixFlowSeqItems(fc);
7081
- const sep6 = fc.end.splice(1, fc.end.length);
7082
- sep6.push(this.sourceToken);
7081
+ const sep7 = fc.end.splice(1, fc.end.length);
7082
+ sep7.push(this.sourceToken);
7083
7083
  const map = {
7084
7084
  type: "block-map",
7085
7085
  offset: fc.offset,
7086
7086
  indent: fc.indent,
7087
- items: [{ start, key: fc, sep: sep6 }]
7087
+ items: [{ start, key: fc, sep: sep7 }]
7088
7088
  };
7089
7089
  this.onKeyLine = true;
7090
7090
  this.stack[this.stack.length - 1] = map;
@@ -14036,6 +14036,11 @@ var import_yaml3 = __toESM(require_dist(), 1);
14036
14036
  import * as fs14 from "node:fs";
14037
14037
  import * as os2 from "node:os";
14038
14038
  import * as path13 from "node:path";
14039
+
14040
+ // src/scripts/ai_council/quorum.ts
14041
+ var SOLO_FLOOR_MIN_PRESENT = 2;
14042
+
14043
+ // src/scripts/ai_council/config.ts
14039
14044
  var _VALID_PROVIDERS = /* @__PURE__ */ new Set([
14040
14045
  "anthropic",
14041
14046
  "openai",
@@ -14342,6 +14347,9 @@ function _build_config(raw, source_path) {
14342
14347
  _asDict(_getOr(raw, "cli_call_budget", {}))
14343
14348
  );
14344
14349
  const quorum = _build_quorum(_get(raw, "quorum", "majority"));
14350
+ const quorum_min_present = _build_quorum_min_present(
14351
+ _get(raw, "quorum_min_present", SOLO_FLOOR_MIN_PRESENT)
14352
+ );
14345
14353
  const necessity_classifier = _build_necessity_classifier(
14346
14354
  _asDict(_getOr(raw, "necessity_classifier", {}))
14347
14355
  );
@@ -14390,6 +14398,7 @@ function _build_config(raw, source_path) {
14390
14398
  consensus_scoring: consensus,
14391
14399
  cli_call_budget,
14392
14400
  quorum,
14401
+ quorum_min_present,
14393
14402
  necessity_classifier,
14394
14403
  model_downgrade,
14395
14404
  debate,
@@ -15217,6 +15226,14 @@ function _build_quorum(raw) {
15217
15226
  `\`quorum\`=${_pyRepr3(raw)} must be 'majority' or an integer >= 1 (got ${_pyTypeName(raw)}).`
15218
15227
  );
15219
15228
  }
15229
+ function _build_quorum_min_present(raw) {
15230
+ if (_isInt(raw) && raw >= 1) {
15231
+ return raw;
15232
+ }
15233
+ throw new CouncilConfigError(
15234
+ `\`quorum_min_present\`=${_pyRepr3(raw)} must be an integer >= 1 (got ${_pyTypeName(raw)}).`
15235
+ );
15236
+ }
15220
15237
  function _build_advisor(name, cfg) {
15221
15238
  if (!_isDict(cfg)) {
15222
15239
  throw new CouncilConfigError(`advisors.${name}: must be a mapping.`);
@@ -20494,35 +20511,109 @@ function foreign_live_records(dir, session_id, options = {}) {
20494
20511
  const own = safe_stem(session_id);
20495
20512
  return read_live_records(dir, options).filter((r) => safe_stem(r.session_id) !== own);
20496
20513
  }
20514
+ function classify_collisions(others, here) {
20515
+ const out = [];
20516
+ for (const r of others) {
20517
+ if (here.roadmap_slug !== null && r.roadmap_slug !== null && r.roadmap_slug === here.roadmap_slug) {
20518
+ out.push({ kind: "roadmap", record: r });
20519
+ }
20520
+ }
20521
+ for (const r of others) {
20522
+ if (here.branch !== null && r.branch === here.branch) {
20523
+ out.push({ kind: "branch", record: r });
20524
+ }
20525
+ }
20526
+ return out;
20527
+ }
20497
20528
 
20498
20529
  // src/scripts/session_register_hook.ts
20499
20530
  var REPLAY_ENV_VAR6 = "AGENT_CONFIG_REPLAY";
20500
- var ROADMAP_CLAIM_REL = path42.join("agents", "runtime", "state", "roadmap-claim.json");
20531
+ var _STATE_REL = path42.join("agents", "runtime", "state");
20532
+ var ROADMAP_CLAIM_REL = path42.join(_STATE_REL, "roadmap-claim.json");
20533
+ function roadmap_claim_rel(session_id) {
20534
+ const id = String(session_id ?? "").trim();
20535
+ if (id === "") {
20536
+ return ROADMAP_CLAIM_REL;
20537
+ }
20538
+ const rel = path42.join(_STATE_REL, `roadmap-claim-${safe_stem(id)}.json`);
20539
+ const norm = path42.normalize(rel);
20540
+ if (!norm.startsWith(`${_STATE_REL}${path42.sep}`) || norm.includes("..")) {
20541
+ return ROADMAP_CLAIM_REL;
20542
+ }
20543
+ return norm;
20544
+ }
20501
20545
  function _is_replay_mode4() {
20502
20546
  return String(process9.env[REPLAY_ENV_VAR6] ?? "").trim() !== "";
20503
20547
  }
20504
- function read_claimed_slug(workspace_root) {
20548
+ function _read_claim_file(file) {
20505
20549
  try {
20506
- const raw = fs41.readFileSync(path42.join(workspace_root, ROADMAP_CLAIM_REL), "utf-8");
20507
- const parsed = JSON.parse(raw);
20508
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
20509
- const slug = parsed["slug"];
20510
- if (typeof slug === "string" && slug.trim().length > 0) {
20511
- return slug.trim();
20512
- }
20550
+ const parsed = JSON.parse(fs41.readFileSync(file, "utf-8"));
20551
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
20552
+ return null;
20513
20553
  }
20514
- return null;
20554
+ const rec = parsed;
20555
+ const slug = rec["slug"];
20556
+ if (typeof slug !== "string" || slug.trim().length === 0) {
20557
+ return null;
20558
+ }
20559
+ const sid = rec["session_id"];
20560
+ return {
20561
+ slug: slug.trim(),
20562
+ ...typeof sid === "string" && sid.trim() !== "" ? { session_id: sid.trim() } : {}
20563
+ };
20515
20564
  } catch {
20516
20565
  return null;
20517
20566
  }
20518
20567
  }
20568
+ function read_claimed_slug(workspace_root, session_id) {
20569
+ const own = roadmap_claim_rel(session_id);
20570
+ if (own !== ROADMAP_CLAIM_REL) {
20571
+ const mine = _read_claim_file(path42.join(workspace_root, own));
20572
+ if (mine !== null) {
20573
+ return mine.slug;
20574
+ }
20575
+ }
20576
+ const legacy = _read_claim_file(path42.join(workspace_root, ROADMAP_CLAIM_REL));
20577
+ if (legacy === null) {
20578
+ return null;
20579
+ }
20580
+ const id = String(session_id ?? "").trim();
20581
+ if (id !== "") {
20582
+ return null;
20583
+ }
20584
+ return legacy.slug;
20585
+ }
20586
+ function claim_is_stale(workspace_root, slug, peer_worktree) {
20587
+ if (slug === null || slug.trim() === "") {
20588
+ return false;
20589
+ }
20590
+ const base = slug.trim().replace(/\.md$/, "");
20591
+ const rel = path42.normalize(path42.join("agents", "roadmaps", `${base}.md`));
20592
+ const inside = rel.startsWith(`agents${path42.sep}roadmaps${path42.sep}`) && !rel.includes("..");
20593
+ if (!inside || path42.isAbsolute(base)) {
20594
+ return true;
20595
+ }
20596
+ const roots = [peer_worktree, workspace_root].filter(
20597
+ (r) => typeof r === "string" && r.trim() !== ""
20598
+ );
20599
+ for (const root of roots) {
20600
+ try {
20601
+ if (fs41.existsSync(path42.join(root, rel))) {
20602
+ return false;
20603
+ }
20604
+ } catch {
20605
+ return false;
20606
+ }
20607
+ }
20608
+ return true;
20609
+ }
20519
20610
  function build_record(workspace_root, session_id, platform, started_at, now = /* @__PURE__ */ new Date()) {
20520
20611
  return {
20521
20612
  session_id,
20522
20613
  platform,
20523
20614
  worktree: workspace_root,
20524
20615
  branch: current_branch(workspace_root),
20525
- roadmap_slug: read_claimed_slug(workspace_root),
20616
+ roadmap_slug: read_claimed_slug(workspace_root, session_id),
20526
20617
  started_at,
20527
20618
  last_seen: iso_now(now)
20528
20619
  };
@@ -20540,20 +20631,26 @@ function foreign_sessions_block(workspace_root, session_id, now = /* @__PURE__ *
20540
20631
  const others = foreign_live_records(dir, session_id, { prune: true, now });
20541
20632
  if (others.length === 0) return null;
20542
20633
  const here = current_branch(workspace_root);
20634
+ const my_slug = read_claimed_slug(workspace_root, session_id);
20543
20635
  const lines = [];
20544
- let collision = null;
20636
+ const live_others = [];
20545
20637
  for (const r of others) {
20546
20638
  const age_min = Math.max(0, Math.round((now.getTime() - Date.parse(r.last_seen)) / 6e4));
20547
- const slug = r.roadmap_slug === null ? "no roadmap claimed" : `roadmap: ${r.roadmap_slug}`;
20639
+ const stale = claim_is_stale(workspace_root, r.roadmap_slug, r.worktree);
20640
+ const slug = r.roadmap_slug === null ? "no roadmap claimed" : stale ? `roadmap: ${r.roadmap_slug} (STALE \u2014 no such open roadmap; treat as no claim)` : `roadmap: ${r.roadmap_slug}`;
20548
20641
  const ttl_note = ttl_is_measured(r.platform) ? "" : ` \xB7 unmeasured host, TTL falls back to ${Math.round(ttl_seconds_for(r.platform) / 3600)}h`;
20549
20642
  lines.push(
20550
20643
  `- ${r.platform} \xB7 branch \`${r.branch ?? "detached"}\` \xB7 ${slug} \xB7 last seen ${age_min} min ago${ttl_note}
20551
20644
  worktree: ${r.worktree}`
20552
20645
  );
20553
- if (collision === null && here !== null && r.branch === here) {
20554
- collision = r;
20555
- }
20646
+ live_others.push(stale ? { ...r, roadmap_slug: null } : r);
20556
20647
  }
20648
+ const collisions = classify_collisions(live_others, {
20649
+ branch: here,
20650
+ roadmap_slug: claim_is_stale(workspace_root, my_slug) ? null : my_slug
20651
+ });
20652
+ const roadmap_hits = collisions.filter((c) => c.kind === "roadmap");
20653
+ const branch_hit = collisions.find((c) => c.kind === "branch") ?? null;
20557
20654
  const parts = [
20558
20655
  "<session-register>",
20559
20656
  "The following OTHER agent sessions are live on this repository right now.",
@@ -20561,7 +20658,23 @@ function foreign_sessions_block(workspace_root, session_id, now = /* @__PURE__ *
20561
20658
  "",
20562
20659
  ...lines
20563
20660
  ];
20564
- if (collision !== null) {
20661
+ if (roadmap_hits.length > 0) {
20662
+ parts.push(
20663
+ "",
20664
+ `DUPLICATE WORK: ${roadmap_hits.length} other live session(s) claim the SAME roadmap \`${my_slug}\`.`,
20665
+ ...roadmap_hits.map(
20666
+ (h) => ` \xB7 branch \`${h.record.branch ?? "detached"}\` in ${h.record.worktree}`
20667
+ ),
20668
+ "A different branch name does NOT make this a different task \u2014 measured twice,",
20669
+ "two sessions built one roadmap phase under two branch names and one PR was wasted.",
20670
+ "STOP before writing code. Ask the user ONCE, as numbered options:",
20671
+ " 1. Take a different roadmap (`agent-config sessions:list` shows what is held)",
20672
+ " 2. Split the roadmap explicitly \u2014 name which phases are yours",
20673
+ " 3. Continue anyway, accepting that one of the two results is discarded",
20674
+ "Never decide silently, in either direction."
20675
+ );
20676
+ }
20677
+ if (branch_hit !== null) {
20565
20678
  parts.push(
20566
20679
  "",
20567
20680
  `COLLISION: another live session already holds branch \`${here}\`.`,
@@ -20602,6 +20715,10 @@ function main25() {
20602
20715
  if (event === "session_end" || event === "stop" && stop_means_session_end(platform)) {
20603
20716
  const dir = register_dir(root);
20604
20717
  if (dir !== null) delete_record(dir, session_id);
20718
+ try {
20719
+ fs41.unlinkSync(path42.join(root, roadmap_claim_rel(session_id)));
20720
+ } catch {
20721
+ }
20605
20722
  return 0;
20606
20723
  }
20607
20724
  if (event === "session_start" || event === "user_prompt_submit" || event === "stop") {
@@ -22528,7 +22645,7 @@ import { fileURLToPath as fileURLToPath36, pathToFileURL as pathToFileURL29 } fr
22528
22645
 
22529
22646
  // src/install/rtkDetection.ts
22530
22647
  import { spawnSync as spawnSync4 } from "node:child_process";
22531
- import { existsSync as existsSync8, statSync as statSync9, readFileSync as readFileSync19, writeFileSync as writeFileSync8, mkdirSync as mkdirSync9 } from "node:fs";
22648
+ import { existsSync as existsSync9, statSync as statSync9, readFileSync as readFileSync19, writeFileSync as writeFileSync8, mkdirSync as mkdirSync9 } from "node:fs";
22532
22649
  import { homedir as homedir4 } from "node:os";
22533
22650
  import { delimiter, join as join19, dirname as dirname9 } from "node:path";
22534
22651
  var TOKEN_KILLER_GAIN_SIGNATURE = /RTK Token Savings/;
@@ -22541,7 +22658,7 @@ function resolveBinaryOnPath(name, pathEnv) {
22541
22658
  if (dir.length === 0) continue;
22542
22659
  for (const suffix of suffixes) {
22543
22660
  const candidate = join19(dir, `${name}${suffix}`);
22544
- if (existsSync8(candidate)) return candidate;
22661
+ if (existsSync9(candidate)) return candidate;
22545
22662
  }
22546
22663
  }
22547
22664
  return null;
@@ -25355,6 +25472,32 @@ function detectLanguage(reply, pinned) {
25355
25472
  reason: `the pinned reply language is "${pinned}" but the reply classifies as "${verdict.language}" (${verdict.de_markers} de / ${verdict.en_markers} en markers; code, quotes, tables, URLs and paths excluded, then scored lead-first by the same classifier that sets the pin)`
25356
25473
  };
25357
25474
  }
25475
+ var _EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
25476
+ var _VERIFY_RE = /\b(test|tests|vitest|jest|pytest|phpunit|pest|tsc|eslint|ruff|mypy|pyright|clippy|typecheck|lint|build|ci|preflight|smoke[-_:a-z]*|check[-_:a-z]*|validate[-_:a-z]*)\b|\b(task|npm|pnpm|yarn|composer|cargo|go|make|php artisan|bun)\s+(run\s+)?\S*(test|check|lint|build|ci|typecheck)/i;
25477
+ function isVerificationCommand(command) {
25478
+ return _VERIFY_RE.test(command);
25479
+ }
25480
+ function detectUnverifiedEdit(toolCalls) {
25481
+ let lastEdit = -1;
25482
+ for (let i = 0; i < toolCalls.length; i += 1) {
25483
+ if (_EDIT_TOOLS.has(toolCalls[i].name)) lastEdit = i;
25484
+ }
25485
+ if (lastEdit === -1) return null;
25486
+ for (let i = lastEdit + 1; i < toolCalls.length; i += 1) {
25487
+ const c = toolCalls[i];
25488
+ if (c.name === "Bash" && c.command !== void 0 && isVerificationCommand(c.command)) {
25489
+ return null;
25490
+ }
25491
+ }
25492
+ const edited = toolCalls[lastEdit].path;
25493
+ return {
25494
+ detector: "verification",
25495
+ // The path, never the diff: the evidence span is quoted into a refusal,
25496
+ // and `ToolCall` is shaped so a file body cannot reach it.
25497
+ evidence: edited ?? toolCalls[lastEdit].name,
25498
+ reason: "this turn changed a file and then ran no verification command \u2014 no test, type-check, lint or build call follows the last edit (verify-before-complete: a claim without a fresh run is unverified)"
25499
+ };
25500
+ }
25358
25501
  function stateDir(workspaceRoot2) {
25359
25502
  return path59.join(workspaceRoot2, "agents", "runtime", "state", "turn-end-gate");
25360
25503
  }
@@ -25387,7 +25530,7 @@ function markRefusedTurn(workspaceRoot2, sessionKey, turnOrdinal, detector) {
25387
25530
  }
25388
25531
  }
25389
25532
  function readTranscriptTail(transcriptPath, opts = {}) {
25390
- const empty = { lastAssistant: "", turnOrdinal: 0 };
25533
+ const empty = { lastAssistant: "", turnOrdinal: 0, toolCalls: [] };
25391
25534
  if (!transcriptPath || !isSafeTranscriptPath(transcriptPath, opts)) return empty;
25392
25535
  let lines;
25393
25536
  try {
@@ -25400,6 +25543,7 @@ function readTranscriptTail(transcriptPath, opts = {}) {
25400
25543
  }
25401
25544
  let lastAssistant = "";
25402
25545
  let turnOrdinal = 0;
25546
+ let toolCalls = [];
25403
25547
  for (const rawLine of lines) {
25404
25548
  const line = rawLine.trim();
25405
25549
  if (!line) continue;
@@ -25416,15 +25560,42 @@ function readTranscriptTail(transcriptPath, opts = {}) {
25416
25560
  if (role !== "assistant" && role !== "user") continue;
25417
25561
  const msg = entry["message"];
25418
25562
  if (typeof msg !== "object" || msg === null || Array.isArray(msg)) continue;
25419
- const text = _messageText(msg["content"]);
25563
+ const content = msg["content"];
25564
+ if (role === "assistant") {
25565
+ toolCalls.push(..._toolCalls(content));
25566
+ }
25567
+ const text = _messageText(content);
25420
25568
  if (text === null) continue;
25421
25569
  if (role === "assistant") {
25422
25570
  lastAssistant = text;
25423
25571
  } else if (!isSyntheticPrompt(text)) {
25424
25572
  turnOrdinal += 1;
25573
+ toolCalls = [];
25425
25574
  }
25426
25575
  }
25427
- return { lastAssistant: lastAssistant.trim(), turnOrdinal };
25576
+ return { lastAssistant: lastAssistant.trim(), turnOrdinal, toolCalls };
25577
+ }
25578
+ function _toolCalls(content) {
25579
+ if (!Array.isArray(content)) return [];
25580
+ const out = [];
25581
+ for (const blk of content) {
25582
+ if (typeof blk !== "object" || blk === null || Array.isArray(blk)) continue;
25583
+ const b = blk;
25584
+ if (b["type"] !== "tool_use") continue;
25585
+ const name = b["name"];
25586
+ if (typeof name !== "string") continue;
25587
+ const input = b["input"];
25588
+ const call = { name };
25589
+ if (typeof input === "object" && input !== null && !Array.isArray(input)) {
25590
+ const inp = input;
25591
+ const cmd = inp["command"];
25592
+ if (typeof cmd === "string") call.command = cmd;
25593
+ const p = inp["file_path"] ?? inp["path"] ?? inp["notebook_path"];
25594
+ if (typeof p === "string") call.path = p;
25595
+ }
25596
+ out.push(call);
25597
+ }
25598
+ return out;
25428
25599
  }
25429
25600
  function _messageText(content) {
25430
25601
  if (typeof content === "string") return content;
@@ -25448,7 +25619,12 @@ function _isOn(v) {
25448
25619
  return null;
25449
25620
  }
25450
25621
  function readGateSettings(workspaceRoot2) {
25451
- const off = { enabled: false, promissory: true, language: true };
25622
+ const off = {
25623
+ enabled: false,
25624
+ promissory: true,
25625
+ language: true,
25626
+ verification: true
25627
+ };
25452
25628
  let settings;
25453
25629
  try {
25454
25630
  settings = load_agent_settings({ cwd: workspaceRoot2 });
@@ -25467,7 +25643,8 @@ function readGateSettings(workspaceRoot2) {
25467
25643
  return {
25468
25644
  enabled: flag("enabled", false),
25469
25645
  promissory: flag("promissory", true),
25470
- language: flag("language", true)
25646
+ language: flag("language", true),
25647
+ verification: flag("verification", true)
25471
25648
  };
25472
25649
  }
25473
25650
  function str4(v) {
@@ -25488,7 +25665,7 @@ function main41() {
25488
25665
  const transcriptPath = str4(
25489
25666
  payload["transcript_path"] ?? payload["transcriptPath"]
25490
25667
  );
25491
- const { lastAssistant, turnOrdinal } = readTranscriptTail(transcriptPath, {
25668
+ const { lastAssistant, turnOrdinal, toolCalls } = readTranscriptTail(transcriptPath, {
25492
25669
  maxBytes: TRANSCRIPT_READ_MAX_BYTES
25493
25670
  });
25494
25671
  if (!lastAssistant) return EXIT_ALLOW17;
@@ -25505,6 +25682,10 @@ function main41() {
25505
25682
  const f = detectLanguage(lastAssistant, readLanguagePin(workspaceRoot2));
25506
25683
  if (f) findings.push(f);
25507
25684
  }
25685
+ if (settings.verification) {
25686
+ const f = detectUnverifiedEdit(toolCalls);
25687
+ if (f) findings.push(f);
25688
+ }
25508
25689
  if (findings.length === 0) return EXIT_ALLOW17;
25509
25690
  markRefusedTurn(workspaceRoot2, sessionKey, turnOrdinal, findings[0].detector);
25510
25691
  const lines = findings.map(
@@ -25514,7 +25695,7 @@ function main41() {
25514
25695
  process.stderr.write(
25515
25696
  `turn-end-gate: REFUSED \u2014 this turn is not finished.
25516
25697
  ${lines.join("\n")}
25517
- Do the promised work now, or correct the language, then end the turn.
25698
+ Do the promised work now, correct the language, or run the verification the edit needs, then end the turn.
25518
25699
  This turn will not be refused a second time.
25519
25700
  Disable: hooks.turn_end_gate.enabled: false in .agent-settings.yml
25520
25701
  `