@wrongstack/core 0.309.1 → 0.310.1

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 (120) hide show
  1. package/dist/chronicle/index.js +113 -41
  2. package/dist/chronicle/metrics-ingest.d.ts +7 -0
  3. package/dist/chronicle/metrics-schema.d.ts +4 -1
  4. package/dist/chronicle/project-server.js +94 -38
  5. package/dist/chronicle/query.d.ts +1 -0
  6. package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
  7. package/dist/chronicle/types.d.ts +2 -0
  8. package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
  9. package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
  10. package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
  11. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
  12. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  13. package/dist/coordination/index.d.ts +5 -1
  14. package/dist/coordination/index.js +2061 -346
  15. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  16. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  17. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  18. package/dist/coordination/mailbox-project-server.js +14 -9
  19. package/dist/core/context.d.ts +35 -44
  20. package/dist/core/conversation-state.d.ts +16 -52
  21. package/dist/core/index.js +53 -16
  22. package/dist/core/provider-runner.d.ts +2 -2
  23. package/dist/core/run-env.d.ts +7 -28
  24. package/dist/core/streaming-response-builder.d.ts +2 -2
  25. package/dist/execution/index.js +111 -153
  26. package/dist/goal/index.js +100 -22
  27. package/dist/hq/auth-store.d.ts +9 -0
  28. package/dist/hq/cost-bridge.d.ts +1 -1
  29. package/dist/hq/index.js +24 -2
  30. package/dist/index.d.ts +12 -5
  31. package/dist/index.js +27808 -33937
  32. package/dist/infrastructure/index.js +210 -132
  33. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  34. package/dist/infrastructure/token-counter.d.ts +5 -1
  35. package/dist/kernel/events/file-events.d.ts +6 -0
  36. package/dist/kernel/events/provider-events.d.ts +10 -7
  37. package/dist/kernel/events/session-events.d.ts +4 -4
  38. package/dist/kernel/events/tool-events.d.ts +12 -2
  39. package/dist/plugin/index.js +105 -35
  40. package/dist/security/index.d.ts +1 -0
  41. package/dist/security/index.js +47 -18
  42. package/dist/security/kanban-boundary.d.ts +1 -1
  43. package/dist/security/kanban-governance-port.d.ts +28 -0
  44. package/dist/session-catalog/index.js +2 -3
  45. package/dist/session-catalog/project-server.js +2 -3
  46. package/dist/session-catalog/protocol.d.ts +1 -1
  47. package/dist/session-catalog/registry.d.ts +1 -1
  48. package/dist/session-catalog/store-schema.d.ts +1 -1
  49. package/dist/session-catalog/store.d.ts +1 -1
  50. package/dist/storage/annotations-store.d.ts +1 -1
  51. package/dist/storage/board-store-port.d.ts +45 -0
  52. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  53. package/dist/storage/config-loader/types.d.ts +1 -1
  54. package/dist/storage/event-bus-port.d.ts +27 -0
  55. package/dist/storage/file-session-writer.d.ts +47 -5
  56. package/dist/storage/goal-coordination.d.ts +1 -1
  57. package/dist/storage/goal-store.d.ts +1 -1
  58. package/dist/storage/index.d.ts +3 -4
  59. package/dist/storage/index.js +1063 -1459
  60. package/dist/storage/plan-store.d.ts +1 -1
  61. package/dist/storage/queue-store.d.ts +1 -1
  62. package/dist/storage/replay-log-store.d.ts +1 -1
  63. package/dist/storage/session-recovery.d.ts +10 -0
  64. package/dist/storage/session-resume-validation.d.ts +13 -1
  65. package/dist/storage/session-store/events.d.ts +1 -1
  66. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  67. package/dist/storage/session-store/rename-session.d.ts +1 -1
  68. package/dist/storage/session-store/resume-session.d.ts +3 -1
  69. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  70. package/dist/storage/session-store/types.d.ts +1 -1
  71. package/dist/storage/session-store.d.ts +70 -0
  72. package/dist/storage/session-summary-tracker.d.ts +8 -0
  73. package/dist/storage/session-write-buffer.d.ts +31 -2
  74. package/dist/storage/task-store.d.ts +1 -1
  75. package/dist/storage/todos-checkpoint.d.ts +1 -1
  76. package/dist/storage/tool-audit-log.d.ts +1 -1
  77. package/dist/tasking/index.js +100 -22
  78. package/dist/tasking/task-tracker.d.ts +24 -1
  79. package/dist/types/compactor.d.ts +2 -2
  80. package/dist/types/config/runtime.d.ts +7 -0
  81. package/dist/types/config/tools.d.ts +30 -0
  82. package/dist/types/context.d.ts +212 -0
  83. package/dist/types/conversation-state.d.ts +109 -0
  84. package/dist/types/error-handler.d.ts +2 -2
  85. package/dist/types/file-event-record.d.ts +6 -0
  86. package/dist/types/index.d.ts +4 -4
  87. package/dist/types/index.js +19 -1
  88. package/dist/types/permission.d.ts +3 -3
  89. package/dist/types/plugin.d.ts +3 -3
  90. package/dist/types/provider-runner.d.ts +2 -2
  91. package/dist/types/provider.d.ts +10 -0
  92. package/dist/types/run-env.d.ts +32 -0
  93. package/dist/types/session.d.ts +3 -0
  94. package/dist/types/slash-command.d.ts +2 -2
  95. package/dist/types/token-counter.d.ts +30 -1
  96. package/dist/types/tool-executor.d.ts +2 -2
  97. package/dist/types/tool.d.ts +5 -5
  98. package/dist/utils/context-breakdown.d.ts +2 -2
  99. package/dist/utils/context-evidence.d.ts +12 -12
  100. package/dist/utils/crash-shield.d.ts +9 -0
  101. package/dist/utils/heap-watchdog.js +11 -3
  102. package/dist/utils/index.d.ts +1 -1
  103. package/dist/utils/index.js +72 -156
  104. package/dist/utils/regex-guard.d.ts +7 -30
  105. package/dist/utils/todos-format.d.ts +1 -1
  106. package/dist/utils/tree-kill.d.ts +2 -0
  107. package/dist/utils/tree-kill.js +1 -0
  108. package/dist/wiring/proxy-rewrite.d.ts +76 -0
  109. package/dist/wiring/proxy-rewrite.js +70 -0
  110. package/instructions/coordination/subagent-baseline.md +10 -0
  111. package/instructions/system-lite.md +2 -0
  112. package/instructions/system-pro.md +40 -0
  113. package/instructions/system.md +15 -0
  114. package/package.json +10 -9
  115. package/dist/defaults/index.d.ts +0 -69
  116. package/dist/defaults/index.js +0 -38133
  117. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  118. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  119. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  120. /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
@@ -488,7 +488,9 @@ function hasToolUseBlock(message) {
488
488
  return message?.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_use");
489
489
  }
490
490
  var ConversationState = class {
491
+ /** Owning context; public for structural typing (Roadmap 10A). */
491
492
  ctx;
493
+ /** Subscribers; public for structural typing (Roadmap 10A). */
492
494
  listeners = /* @__PURE__ */ new Set();
493
495
  _revision = 0;
494
496
  constructor(ctx) {
@@ -543,9 +545,8 @@ var ConversationState = class {
543
545
  }
544
546
  /**
545
547
  * How many of the oldest messages must be dropped to satisfy both retention
546
- * caps — count ({@link Context.MAX_MESSAGES}) and size
547
- * ({@link Context.MAX_MESSAGE_TOKENS}). Returns 0 when the history already
548
- * fits, which is the overwhelmingly common case.
548
+ * caps — count and size (see {@link AgentContext.messageLimits}). Returns 0
549
+ * when the history already fits, which is the overwhelmingly common case.
549
550
  *
550
551
  * The size pass reads the per-message `_estTokens` cache populated at
551
552
  * mutation time, so it is a sum over numbers rather than a re-walk of
@@ -553,9 +554,7 @@ var ConversationState = class {
553
554
  * cap determines the starting index for the sum but does not gate it.
554
555
  */
555
556
  overflowCount(arr) {
556
- const contextClass = this.ctx.constructor;
557
- const maxMessages = contextClass.MAX_MESSAGES;
558
- const maxMessageTokens = contextClass.MAX_MESSAGE_TOKENS;
557
+ const { maxMessages, maxMessageTokens } = this.ctx.messageLimits;
559
558
  let drop = maxMessages > 0 ? Math.max(0, arr.length - maxMessages) : 0;
560
559
  if (maxMessageTokens <= 0) return this.protocolSafeDropCount(arr, drop);
561
560
  let total = 0;
@@ -826,6 +825,10 @@ var Context = class _Context {
826
825
  * so storage operations can include it in `storage.*` events.
827
826
  */
828
827
  traceId;
828
+ /** Logical provider request whose response produced the current tool calls. */
829
+ activeLogicalRequestId = void 0;
830
+ /** Content-addressed prompt composition for {@link activeLogicalRequestId}. */
831
+ activePromptManifestId = void 0;
829
832
  /**
830
833
  * Session id pinned to the currently-executing run. Set by `Agent.run()`
831
834
  * at run start and cleared when the run ends. Event-emission sites must
@@ -851,6 +854,7 @@ var Context = class _Context {
851
854
  return resolveEventSessionId(this);
852
855
  }
853
856
  /** Callbacks fired when `setWorkingDir()` changes the working directory. */
857
+ /** WorkingDir-change callbacks; public for structural typing (Roadmap 10A). */
854
858
  _onWorkingDirChanged = [];
855
859
  /**
856
860
  * Serializes externally requested provider/model changes. Request creation
@@ -1106,7 +1110,16 @@ var Context = class _Context {
1106
1110
  * For hooks that must survive across run boundaries (mailbox heartbeat,
1107
1111
  * awareness polling, HQ publisher), prefer `registerAgentHook` instead.
1108
1112
  */
1113
+ /** Run-scoped abort hooks (drained by drainAbortHooks). Public for structural typing (Roadmap 10A). */
1109
1114
  abortHooks = /* @__PURE__ */ new Set();
1115
+ /** Retention limits honoring runtime subclass overrides of the statics. */
1116
+ get messageLimits() {
1117
+ const cls = this.constructor;
1118
+ return Object.freeze({
1119
+ maxMessages: cls.MAX_MESSAGES,
1120
+ maxMessageTokens: cls.MAX_MESSAGE_TOKENS
1121
+ });
1122
+ }
1110
1123
  registerAbortHook(fn) {
1111
1124
  this.abortHooks.add(fn);
1112
1125
  return () => this.abortHooks.delete(fn);
@@ -1131,6 +1144,7 @@ var Context = class _Context {
1131
1144
  * awareness polling interval, HQ publisher connection, and auto-compaction
1132
1145
  * timer — resources that must survive from the first run to the last.
1133
1146
  */
1147
+ /** Session-lifetime teardown hooks (drained by drainAgentHooks). Public for structural typing (Roadmap 10A). */
1134
1148
  agentHooks = /* @__PURE__ */ new Set();
1135
1149
  registerAgentHook(fn) {
1136
1150
  this.agentHooks.add(fn);
@@ -1311,6 +1325,9 @@ var Context = class _Context {
1311
1325
  agentName: this.agentName,
1312
1326
  provider: typeof this.provider === "object" ? this.provider.id : String(this.provider),
1313
1327
  model: this.model,
1328
+ ...this.activeLogicalRequestId ? { logicalRequestId: this.activeLogicalRequestId } : {},
1329
+ ...this.activePromptManifestId ? { promptManifestId: this.activePromptManifestId } : {},
1330
+ provenanceConfidence: this.activeLogicalRequestId && this.activePromptManifestId ? "explicit" : "unknown",
1314
1331
  toolName: input.toolName,
1315
1332
  toolUseId: input.toolUseId,
1316
1333
  scope,
@@ -1338,6 +1355,9 @@ var Context = class _Context {
1338
1355
  agentName: this.agentName,
1339
1356
  provider: record.provider,
1340
1357
  model: record.model,
1358
+ ...record.logicalRequestId ? { logicalRequestId: record.logicalRequestId } : {},
1359
+ ...record.promptManifestId ? { promptManifestId: record.promptManifestId } : {},
1360
+ provenanceConfidence: record.provenanceConfidence,
1341
1361
  toolName: input.toolName,
1342
1362
  toolUseId: input.toolUseId,
1343
1363
  scope,
@@ -3570,127 +3590,11 @@ function assertNever(x, message) {
3570
3590
  }
3571
3591
 
3572
3592
  // src/utils/regex-guard.ts
3573
- var MAX_PATTERN_LEN = 256;
3574
- var DANGEROUS_PATTERNS = [
3575
- // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
3576
- /(\([^)]*[+*][^)]*\))[+*]/,
3577
- /(\(\?:[^)]*[+*][^)]*\))[+*]/,
3578
- // Adjacent quantifiers: a++ a*+
3579
- /[+*]{2,}/,
3580
- // Quantifier on alternation with length 2+
3581
- /\([^|)]+\|[^)]+\)[+*][+*]/,
3582
- // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
3583
- /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
3584
- ];
3585
- function hasAmbiguousQuantifiedAlternation(pattern) {
3586
- for (let i = 0; i < pattern.length; i++) {
3587
- if (pattern[i] !== "(") continue;
3588
- if (i > 0 && pattern[i - 1] === "\\") continue;
3589
- let depth = 0;
3590
- let inClass = false;
3591
- let j = i;
3592
- for (; j < pattern.length; j++) {
3593
- const ch = pattern[j];
3594
- if (ch === "\\") {
3595
- j++;
3596
- continue;
3597
- }
3598
- if (inClass) {
3599
- if (ch === "]") inClass = false;
3600
- continue;
3601
- }
3602
- if (ch === "[") {
3603
- inClass = true;
3604
- continue;
3605
- }
3606
- if (ch === "(") depth++;
3607
- else if (ch === ")") {
3608
- depth--;
3609
- if (depth === 0) break;
3610
- }
3611
- }
3612
- if (j >= pattern.length) return false;
3613
- const next = pattern[j + 1];
3614
- if (next !== "+" && next !== "*" && next !== "{") continue;
3615
- let inner = pattern.slice(i + 1, j);
3616
- inner = inner.replace(/^\?(?::|<?[=!])/u, "");
3617
- const branches = [];
3618
- let current = "";
3619
- let d = 0;
3620
- let cls = false;
3621
- for (let k = 0; k < inner.length; k++) {
3622
- const ch = inner[k];
3623
- if (ch === "\\") {
3624
- current += ch + (inner[k + 1] ?? "");
3625
- k++;
3626
- continue;
3627
- }
3628
- if (cls) {
3629
- if (ch === "]") cls = false;
3630
- current += ch;
3631
- continue;
3632
- }
3633
- if (ch === "[") {
3634
- cls = true;
3635
- current += ch;
3636
- continue;
3637
- }
3638
- if (ch === "(") d++;
3639
- if (ch === ")") d--;
3640
- if (ch === "|" && d === 0) {
3641
- branches.push(current);
3642
- current = "";
3643
- continue;
3644
- }
3645
- current += ch;
3646
- }
3647
- branches.push(current);
3648
- if (branches.length < 2) continue;
3649
- for (let a = 0; a < branches.length; a++) {
3650
- for (let b = a + 1; b < branches.length; b++) {
3651
- const x = branches[a];
3652
- const y = branches[b];
3653
- if (x === "" || y === "") return true;
3654
- if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
3655
- }
3656
- }
3657
- }
3658
- return false;
3659
- }
3660
- function compileUserRegex(pattern, flags) {
3661
- if (typeof pattern !== "string") {
3662
- return { ok: false, reason: "pattern must be a string" };
3663
- }
3664
- if (pattern.length === 0) {
3665
- return { ok: false, reason: "pattern is empty" };
3666
- }
3667
- if (pattern.length > MAX_PATTERN_LEN) {
3668
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
3669
- }
3670
- for (const rx of DANGEROUS_PATTERNS) {
3671
- if (rx.test(pattern)) {
3672
- return {
3673
- ok: false,
3674
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
3675
- };
3676
- }
3677
- }
3678
- if (hasAmbiguousQuantifiedAlternation(pattern)) {
3679
- return {
3680
- ok: false,
3681
- reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
3682
- };
3683
- }
3684
- try {
3685
- return { ok: true, regex: new RegExp(pattern, flags) };
3686
- } catch (err) {
3687
- return {
3688
- ok: false,
3689
- reason: err instanceof Error ? err.message : "invalid regex"
3690
- };
3691
- }
3692
- }
3693
- var MAX_SUBJECT_LEN = 64 * 1024;
3593
+ import {
3594
+ capSubject,
3595
+ compileUserRegex,
3596
+ MAX_SUBJECT_LEN
3597
+ } from "@wrongstack/primitives";
3694
3598
 
3695
3599
  // src/execution/autonomous-runner.ts
3696
3600
  var DoneConditionChecker = class {
@@ -5088,7 +4992,12 @@ function isContextOverflowShaped(err) {
5088
4992
  const providerErr = err;
5089
4993
  if (providerErr.kind === "context_overflow" || providerErr.status === 413) return true;
5090
4994
  if (providerErr.status < 400) return false;
5091
- const text = [providerErr.message, providerErr.body?.message, providerErr.body?.type, providerErr.body?.raw].filter(Boolean).join("\n");
4995
+ const text = [
4996
+ providerErr.message,
4997
+ providerErr.body?.message,
4998
+ providerErr.body?.type,
4999
+ providerErr.body?.raw
5000
+ ].filter(Boolean).join("\n");
5092
5001
  return CONTEXT_OVERFLOW_RE.test(text);
5093
5002
  }
5094
5003
  function describeStatus(status, type) {
@@ -10393,27 +10302,39 @@ var DefaultErrorHandler = class {
10393
10302
  import { execFile } from "node:child_process";
10394
10303
  import { promisify } from "node:util";
10395
10304
 
10396
- // src/storage/goal-coordination.ts
10397
- import { updateTask } from "@wrongstack/kanban";
10305
+ // src/storage/board-store-port.ts
10306
+ var notWired = () => {
10307
+ throw new Error(
10308
+ "Kanban BoardStorePort is not wired \u2014 register the implementation at the CLI composition root (see setBoardStorePort)."
10309
+ );
10310
+ };
10311
+ var port = void 0;
10312
+ function boardStore() {
10313
+ return port ?? notWired();
10314
+ }
10315
+ function tryBoardStore() {
10316
+ return port ?? null;
10317
+ }
10398
10318
 
10399
10319
  // src/storage/goal-kanban.ts
10400
- import { addTask, createBoard, getBoard, listBoards, removeBoard } from "@wrongstack/kanban";
10401
10320
  var GOAL_BOARD_TAG_PREFIX = "goal:";
10402
10321
  async function findGoalKanbanBoard(projectRoot, boardId) {
10403
10322
  if (!projectRoot || !boardId) return null;
10404
10323
  try {
10405
- return await getBoard(projectRoot, boardId);
10324
+ return await boardStore().getBoard(projectRoot, boardId);
10406
10325
  } catch {
10407
10326
  return null;
10408
10327
  }
10409
10328
  }
10410
10329
  async function findGoalBoardByTag(projectRoot, goalFile) {
10411
10330
  if (!projectRoot) return null;
10331
+ const store = tryBoardStore();
10332
+ if (!store) return null;
10412
10333
  const tag = goalTag(goalFile);
10413
- const boards = await listBoards(projectRoot);
10334
+ const boards = await store.listBoards(projectRoot);
10414
10335
  const matched = boards.find((b) => b.tags?.includes(tag));
10415
10336
  if (!matched) return null;
10416
- return getBoard(projectRoot, matched.id).catch(() => null);
10337
+ return store.getBoard(projectRoot, matched.id).catch(() => null);
10417
10338
  }
10418
10339
  function goalTag(goalFile) {
10419
10340
  const goal = (goalFile.refinedGoal || goalFile.goal).replace(/\s+/g, "-").slice(0, 24).toLowerCase();
@@ -10577,7 +10498,7 @@ async function refreshGoalKanban(projectRoot, goal, completed, resolvedBoard) {
10577
10498
  const originMatch = task.origin?.system === "goal" && task.origin?.taskId ? originKeys.has(task.origin.taskId) : false;
10578
10499
  const titleMatch = !task.origin?.taskId && completedTitles.has(normalizeTitle(task.title));
10579
10500
  if (!originMatch && !titleMatch) continue;
10580
- const result = await updateTask(
10501
+ const result = await boardStore().updateTask(
10581
10502
  projectRoot,
10582
10503
  board.id,
10583
10504
  task.id,
@@ -22043,6 +21964,8 @@ function logToolSuccess(opts, ctx, use, toolName, durationMs, outputChars) {
22043
21964
  id: use.id,
22044
21965
  sessionId: ctx.session.id,
22045
21966
  ...ctx.traceId ? { traceId: ctx.traceId } : {},
21967
+ ...ctx.activeLogicalRequestId ? { logicalRequestId: ctx.activeLogicalRequestId } : {},
21968
+ ...ctx.activePromptManifestId ? { promptManifestId: ctx.activePromptManifestId } : {},
22046
21969
  agentId: ctx.agentId ?? "<unknown>",
22047
21970
  durationMs,
22048
21971
  outputChars
@@ -22067,6 +21990,8 @@ function logToolFailure(opts, ctx, use, toolName, durationMs, err) {
22067
21990
  id: use.id,
22068
21991
  sessionId: ctx.session.id,
22069
21992
  ...ctx.traceId ? { traceId: ctx.traceId } : {},
21993
+ ...ctx.activeLogicalRequestId ? { logicalRequestId: ctx.activeLogicalRequestId } : {},
21994
+ ...ctx.activePromptManifestId ? { promptManifestId: ctx.activePromptManifestId } : {},
22070
21995
  agentId: ctx.agentId ?? "<unknown>",
22071
21996
  durationMs,
22072
21997
  category,
@@ -22136,14 +22061,20 @@ import { isDeepStrictEqual } from "node:util";
22136
22061
  // src/security/kanban-boundary.ts
22137
22062
  import { realpath } from "node:fs/promises";
22138
22063
  import * as path17 from "node:path";
22139
- import {
22140
- evaluateContractGraphReadiness,
22141
- evaluateKanbanBoundaryOpaque,
22142
- evaluateKanbanBoundaryPath,
22143
- readBoard,
22144
- resolveKanbanBoundaryLayers
22145
- } from "@wrongstack/kanban";
22146
- var GOVERNANCE_CONTROL_TOOLS = /* @__PURE__ */ new Set(["kanban", "plan", "task", "todo"]);
22064
+
22065
+ // src/security/kanban-governance-port.ts
22066
+ var notWired2 = () => {
22067
+ throw new Error(
22068
+ "KanbanGovernancePort is not wired \u2014 register the implementation at the CLI composition root (see setKanbanGovernance)."
22069
+ );
22070
+ };
22071
+ var port2 = void 0;
22072
+ function kanbanGovernance() {
22073
+ return port2 ?? notWired2();
22074
+ }
22075
+
22076
+ // src/security/kanban-boundary.ts
22077
+ var GOVERNANCE_CONTROL_TOOLS = /* @__PURE__ */ new Set(["kanban", "plan", "task", "todo", "nextsteps"]);
22147
22078
  var PATH_KEYS = /* @__PURE__ */ new Set([
22148
22079
  "path",
22149
22080
  "paths",
@@ -22178,7 +22109,10 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
22178
22109
  reason: "Kanban governance is mandatory before product mutation. Create a managed card, add its required details and executable acceptance criteria, then call kanban start_task to bind it to this run."
22179
22110
  } : { decision: "allow" };
22180
22111
  }
22181
- const board = await readBoard(identity.policyRoot ?? ctx.projectRoot, identity.boardId);
22112
+ const board = await kanbanGovernance().readBoard(
22113
+ identity.policyRoot ?? ctx.projectRoot,
22114
+ identity.boardId
22115
+ );
22182
22116
  if (!board) {
22183
22117
  return governanceRequired ? {
22184
22118
  decision: "block",
@@ -22204,22 +22138,39 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
22204
22138
  taskId: identity.taskId
22205
22139
  };
22206
22140
  }
22207
- const readiness = evaluateContractGraphReadiness(board, task.id);
22141
+ const readiness = kanbanGovernance().evaluateContractGraphReadiness(board, task.id);
22208
22142
  if (!readiness.ready) {
22143
+ const issueLines = readiness.issues.map((issue, index) => ` ${index + 1}. ${issue.message}`).join("\n");
22209
22144
  return {
22210
22145
  decision: "block",
22211
- reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
22146
+ reason: `Active card failed contract-readiness check (${readiness.issues.length} issue(s)):
22147
+ ` + issueLines + `
22148
+ Resolve the readiness issues, then call kanban start_task to retry.`,
22212
22149
  boardId: board.id,
22213
22150
  taskId: task.id,
22214
22151
  readinessIssues: readiness.issues
22215
22152
  };
22216
22153
  }
22217
- if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
22218
- const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
22219
- const assignmentStatus = task.assignment?.status ?? "missing";
22154
+ const lifecycleStage = task.lifecycle?.currentStage;
22155
+ const assignmentStatus = task.assignment?.status;
22156
+ const lifecycleOk = lifecycleStage === "running";
22157
+ const assignmentOk = assignmentStatus === "running";
22158
+ if (!lifecycleOk || !assignmentOk) {
22159
+ const failed = [];
22160
+ let reassessmentNote = "";
22161
+ if (!lifecycleOk) {
22162
+ failed.push(`lifecycle.currentStage (got: ${lifecycleStage ?? "missing"}; want: 'running')`);
22163
+ }
22164
+ if (!assignmentOk) {
22165
+ failed.push(`assignment.status (got: ${assignmentStatus ?? "missing"}; want: 'running')`);
22166
+ if (lifecycleOk && (assignmentStatus === "queued" || assignmentStatus === void 0 || assignmentStatus === null)) {
22167
+ reassessmentNote = '\nNote: lifecycle is still "running" but the assignment was cleared \u2014 the automated kanban reassessment agent likely reset the binding. Re-issue kanban start_task to re-bind the leader to this card.';
22168
+ }
22169
+ }
22220
22170
  return {
22221
22171
  decision: "block",
22222
- reason: `Active card must be in Running with a live assignment before product mutation (lifecycle: ${lifecycleStage}; assignment: ${assignmentStatus}). Call kanban start_task after completing the required card details.`,
22172
+ reason: `Active card failed ${failed.length} governance check(s) before product mutation:
22173
+ ` + failed.map((line, index) => ` ${index + 1}. ${line}`).join("\n") + reassessmentNote,
22223
22174
  boardId: board.id,
22224
22175
  taskId: task.id
22225
22176
  };
@@ -22237,7 +22188,7 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
22237
22188
  };
22238
22189
  }
22239
22190
  }
22240
- const layers = resolveKanbanBoundaryLayers(board, task);
22191
+ const layers = kanbanGovernance().resolveKanbanBoundaryLayers(board, task);
22241
22192
  if (layers.length === 0) return { decision: "allow" };
22242
22193
  const caps = tool.capabilities ?? [];
22243
22194
  const access2 = caps.includes("fs.write") || caps.includes("fs.write.outside-project") ? "write" : caps.includes("fs.read") ? "read" : void 0;
@@ -22245,10 +22196,10 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
22245
22196
  const candidates = access2 ? await extractCandidatePaths(tool.name, input, ctx) : [];
22246
22197
  let evaluation;
22247
22198
  if (shellLike || access2 && candidates.length === 0) {
22248
- evaluation = evaluateKanbanBoundaryOpaque(layers, tool.name);
22199
+ evaluation = kanbanGovernance().evaluateKanbanBoundaryOpaque(layers, tool.name);
22249
22200
  } else if (access2) {
22250
22201
  const results = candidates.map(
22251
- (candidate) => evaluateKanbanBoundaryPath(layers, candidate, access2)
22202
+ (candidate) => kanbanGovernance().evaluateKanbanBoundaryPath(layers, candidate, access2)
22252
22203
  );
22253
22204
  evaluation = results.find((result) => result.decision === "block") ?? results.find((result) => result.decision === "confirm") ?? { decision: "allow" };
22254
22205
  } else {
@@ -22775,6 +22726,8 @@ var ToolExecutor = class _ToolExecutor {
22775
22726
  this.opts.events?.emit("permission.evaluated", {
22776
22727
  sessionId: resolveEventSessionId(ctx),
22777
22728
  ...ctx.traceId ? { traceId: ctx.traceId } : {},
22729
+ ...ctx.activeLogicalRequestId ? { logicalRequestId: ctx.activeLogicalRequestId } : {},
22730
+ ...ctx.activePromptManifestId ? { promptManifestId: ctx.activePromptManifestId } : {},
22778
22731
  ...ctx.agentId ? { agentId: ctx.agentId } : {},
22779
22732
  name: tool.name,
22780
22733
  id: use.id,
@@ -22926,6 +22879,9 @@ ${post.additionalContext}`;
22926
22879
  agentName: ctx.agentName,
22927
22880
  provider: typeof ctx.provider === "object" ? ctx.provider.id : String(ctx.provider),
22928
22881
  model: ctx.model,
22882
+ ...ctx.activeLogicalRequestId ? { logicalRequestId: ctx.activeLogicalRequestId } : {},
22883
+ ...ctx.activePromptManifestId ? { promptManifestId: ctx.activePromptManifestId } : {},
22884
+ provenanceConfidence: ctx.activeLogicalRequestId && ctx.activePromptManifestId ? "explicit" : "unknown",
22929
22885
  toolName: tool.name,
22930
22886
  toolUseId: use.id,
22931
22887
  scope: ctx.currentKanbanTaskId ? "task" : "session",
@@ -23068,6 +23024,8 @@ ${preToolContext.text}` : preToolContext.text;
23068
23024
  this.opts.events?.emit("tool.started", {
23069
23025
  sessionId: resolveEventSessionId(ctx),
23070
23026
  ...ctx.traceId ? { traceId: ctx.traceId } : {},
23027
+ ...ctx.activeLogicalRequestId ? { logicalRequestId: ctx.activeLogicalRequestId } : {},
23028
+ ...ctx.activePromptManifestId ? { promptManifestId: ctx.activePromptManifestId } : {},
23071
23029
  agentId: ctx.agentId,
23072
23030
  agentName: ctx.agentName,
23073
23031
  name: tool.name,
@@ -207,6 +207,13 @@ var TaskTracker = class _TaskTracker {
207
207
  graph = null;
208
208
  transitions = [];
209
209
  listeners = [];
210
+ /**
211
+ * Re-entrancy guard for status cascades: true while an auto-block /
212
+ * auto-unblock cascade is running. Listener-driven mutations during a
213
+ * cascade still record their own primary transition — only their
214
+ * cascading is skipped (prevents unbounded recursion).
215
+ */
216
+ cascading = false;
210
217
  static MAX_TRANSITIONS = 1e4;
211
218
  /**
212
219
  * Subscribe to live task mutations (add / update / status change). Returns an
@@ -282,6 +289,28 @@ var TaskTracker = class _TaskTracker {
282
289
  message: "No graph loaded",
283
290
  code: ERROR_CODES.SDD_INVALID_STATE
284
291
  });
292
+ if (!this.graph.nodes.has(from) || !this.graph.nodes.has(to)) {
293
+ throw new SddError({
294
+ message: `Cannot add ${type} edge ${from} -> ${to}: endpoint task is not in the graph`,
295
+ code: ERROR_CODES.SDD_NOT_READY,
296
+ context: { from, to, type }
297
+ });
298
+ }
299
+ if (from === to) {
300
+ throw new SddError({
301
+ message: `Cannot add ${type} edge: self-loop on task ${from}`,
302
+ code: ERROR_CODES.SDD_INVALID_STATE,
303
+ context: { nodeId: from, type }
304
+ });
305
+ }
306
+ if (this.graph.edges.some((e) => e.from === from && e.to === to && e.type === type)) return;
307
+ if (type === "depends_on" && this.dependsOnTransitively(from, to, /* @__PURE__ */ new Set())) {
308
+ throw new SddError({
309
+ message: `Cannot add depends_on edge ${from} -> ${to}: it would create a dependency cycle`,
310
+ code: ERROR_CODES.SDD_INVALID_STATE,
311
+ context: { from, to }
312
+ });
313
+ }
285
314
  this.graph.edges.push({
286
315
  id: crypto.randomUUID(),
287
316
  from,
@@ -382,7 +411,7 @@ var TaskTracker = class _TaskTracker {
382
411
  if (status === "in_progress") {
383
412
  node.startedAt = now;
384
413
  }
385
- this.transitions.push({ from, to: status, timestamp: now, reason });
414
+ this.transitions.push({ nodeId: id, from, to: status, timestamp: now, reason });
386
415
  if (this.transitions.length > _TaskTracker.MAX_TRANSITIONS) {
387
416
  this.transitions.splice(0, this.transitions.length - _TaskTracker.MAX_TRANSITIONS);
388
417
  }
@@ -398,7 +427,7 @@ var TaskTracker = class _TaskTracker {
398
427
  type: "status_changed",
399
428
  nodeId: id,
400
429
  node,
401
- transition: { from, to: status, timestamp: now, reason }
430
+ transition: { nodeId: id, from, to: status, timestamp: now, reason }
402
431
  });
403
432
  }
404
433
  updateNode(id, patch) {
@@ -489,40 +518,89 @@ var TaskTracker = class _TaskTracker {
489
518
  }
490
519
  return computeTaskProgress(this.graph);
491
520
  }
492
- getTransitions(_taskId) {
493
- return [...this.transitions];
521
+ /**
522
+ * Recorded transitions in insertion order (oldest first — the array is
523
+ * trimmed from the front). With `taskId`, only that task's transitions are
524
+ * returned, still oldest-first; a consumer wanting newest-first reverses
525
+ * the result. Per-task filtering is possible because every TaskTransition
526
+ * carries `nodeId` (BIZ-005).
527
+ */
528
+ getTransitions(taskId) {
529
+ return taskId === void 0 ? [...this.transitions] : this.transitions.filter((t) => t.nodeId === taskId);
530
+ }
531
+ /**
532
+ * Record a cascade-driven status change (BIZ-003): mutate the node, push a
533
+ * TaskTransition, persist, and notify subscribers — the same effects an
534
+ * explicit updateNodeStatus produces — so the board projector's live
535
+ * snapshot and the transition log observe auto-block/auto-unblock.
536
+ */
537
+ recordCascadeStatusChange(nodeId, node, to, reason) {
538
+ const from = node.status;
539
+ if (from === to) return;
540
+ const now = Date.now();
541
+ node.status = to;
542
+ node.updatedAt = now;
543
+ if (this.graph) this.graph.updatedAt = now;
544
+ this.transitions.push({ nodeId, from, to, timestamp: now, reason });
545
+ if (this.transitions.length > _TaskTracker.MAX_TRANSITIONS) {
546
+ this.transitions.splice(0, this.transitions.length - _TaskTracker.MAX_TRANSITIONS);
547
+ }
548
+ this.persist();
549
+ this.notifyChange({
550
+ type: "status_changed",
551
+ nodeId,
552
+ node,
553
+ transition: { nodeId, from, to, timestamp: now, reason }
554
+ });
494
555
  }
495
556
  unblockDependents(completedId) {
496
- if (!this.graph) return;
497
- const dependents = this.getDependents(completedId);
498
- for (const depId of dependents) {
499
- const dep = this.graph.nodes.get(depId);
500
- if (dep?.status === "blocked") {
557
+ if (!this.graph || this.cascading) return;
558
+ this.cascading = true;
559
+ try {
560
+ const dependents = this.getDependents(completedId);
561
+ for (const depId of dependents) {
562
+ const dep = this.graph.nodes.get(depId);
563
+ if (dep?.status !== "blocked") continue;
501
564
  const remainingBlockers = this.getBlockers(depId);
502
565
  const allUnblocked = remainingBlockers.every((id) => {
503
566
  const blocker = this.graph?.nodes.get(id);
504
567
  return blocker?.status === "completed" || blocker?.status === "failed";
505
568
  });
506
569
  if (allUnblocked) {
507
- dep.status = "pending";
508
- dep.updatedAt = Date.now();
570
+ this.recordCascadeStatusChange(
571
+ depId,
572
+ dep,
573
+ "pending",
574
+ `auto-unblocked: blocker ${completedId} reached a terminal state`
575
+ );
509
576
  }
510
577
  }
578
+ } finally {
579
+ this.cascading = false;
511
580
  }
512
581
  }
513
582
  checkAndBlockIfNeeded(taskId) {
514
- if (!this.graph) return;
515
- const blockers = this.getBlockers(taskId);
516
- const someBlocked = blockers.some((id) => {
517
- const blocker = this.graph?.nodes.get(id);
518
- return blocker?.status !== "completed" && blocker?.status !== "failed";
519
- });
520
- if (someBlocked) {
521
- const node = this.graph.nodes.get(taskId);
522
- if (node) {
523
- node.status = "blocked";
524
- node.updatedAt = Date.now();
583
+ if (!this.graph || this.cascading) return;
584
+ this.cascading = true;
585
+ try {
586
+ const blockers = this.getBlockers(taskId);
587
+ const incomplete = blockers.filter((id) => {
588
+ const blocker = this.graph?.nodes.get(id);
589
+ return blocker?.status !== "completed" && blocker?.status !== "failed";
590
+ });
591
+ if (incomplete.length > 0) {
592
+ const node = this.graph.nodes.get(taskId);
593
+ if (node) {
594
+ this.recordCascadeStatusChange(
595
+ taskId,
596
+ node,
597
+ "blocked",
598
+ `auto-blocked: incomplete blocker(s) ${incomplete.join(", ")}`
599
+ );
600
+ }
525
601
  }
602
+ } finally {
603
+ this.cascading = false;
526
604
  }
527
605
  }
528
606
  /**
@@ -167,6 +167,15 @@ export interface HqAuthFile {
167
167
  * reveal unused codes. Each code is consumed on first successful use.
168
168
  */
169
169
  totpRecoveryCodes?: string[];
170
+ /**
171
+ * Highest TOTP time-step counter already consumed by a successful 2FA
172
+ * verification (SEC-002: persisted to prevent replay after restart).
173
+ * Written by the 2FA verification route (`totp-routes.ts`) — not the
174
+ * initial password check — and read there to reject any counter <= this
175
+ * value, so an observed code cannot be replayed within its validity
176
+ * window even across a server restart.
177
+ */
178
+ totpLastUsedCounter?: number;
170
179
  /**
171
180
  * Operator-configured alert-rule thresholds. When present, these override
172
181
  * the built-in defaults for the alert engine (cost ceiling, stale-machine
@@ -16,8 +16,8 @@
16
16
  * @module hq/cost-bridge
17
17
  */
18
18
  import type { EventBus } from '../kernel/events.js';
19
- import type { HqEventEnvelope } from './protocol.js';
20
19
  import { type BridgeContextOptions } from './bridge-context.js';
20
+ import type { HqEventEnvelope } from './protocol.js';
21
21
  export interface CostTelemetryBridgeOptions extends BridgeContextOptions {
22
22
  /** Local EventBus emitting `token.accounted`. */
23
23
  events: EventBus;