@indexnetwork/protocol 6.15.0-rc.407.1 → 8.0.2-rc.418.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 (110) hide show
  1. package/CHANGELOG.md +309 -0
  2. package/IMPLEMENTATION.md +7 -4
  3. package/dist/capabilities/opportunities.facade.d.ts +4 -0
  4. package/dist/capabilities/opportunities.facade.js +4 -0
  5. package/dist/chat/chat-streaming.types.d.ts +4 -5
  6. package/dist/chat/chat.agent.d.ts +0 -19
  7. package/dist/chat/chat.agent.js +1 -70
  8. package/dist/chat/chat.persona.d.ts +0 -6
  9. package/dist/chat/chat.persona.js +0 -1
  10. package/dist/chat/chat.prompt.js +10 -10
  11. package/dist/chat/chat.prompt.modules.js +0 -104
  12. package/dist/chat/chat.tools.js +11 -7
  13. package/dist/chat/negotiator.persona.js +0 -1
  14. package/dist/chat/onboarding.persona.js +0 -1
  15. package/dist/chat/reporter.persona.d.ts +1 -1
  16. package/dist/chat/reporter.persona.js +1 -2
  17. package/dist/chat/reporter.prompt.js +5 -5
  18. package/dist/chat/signal.persona.js +0 -1
  19. package/dist/communities/application/network.tools.js +48 -27
  20. package/dist/contacts/application/contact.tools.js +9 -9
  21. package/dist/index.d.ts +9 -2
  22. package/dist/index.js +5 -0
  23. package/dist/maintenance/maintenance.graph.js +7 -0
  24. package/dist/mcp/mcp.authorization-policy.d.ts +250 -0
  25. package/dist/mcp/mcp.authorization-policy.js +522 -0
  26. package/dist/mcp/mcp.server.d.ts +6 -29
  27. package/dist/mcp/mcp.server.js +202 -228
  28. package/dist/negotiation/application/negotiation.agent.js +29 -5
  29. package/dist/negotiation/application/negotiation.graph.d.ts +2 -5
  30. package/dist/negotiation/application/negotiation.graph.js +53 -23
  31. package/dist/negotiation/domain/index.d.ts +3 -0
  32. package/dist/negotiation/domain/index.js +3 -0
  33. package/dist/negotiation/domain/negotiation.deadlock.d.ts +7 -1
  34. package/dist/negotiation/domain/negotiation.deadlock.js +48 -1
  35. package/dist/negotiation/domain/negotiation.stance.contracts.d.ts +85 -0
  36. package/dist/negotiation/domain/negotiation.stance.contracts.js +140 -0
  37. package/dist/negotiation/domain/negotiation.state.d.ts +2 -3
  38. package/dist/negotiation/domain/negotiation.state.js +2 -3
  39. package/dist/negotiation/public/index.d.ts +2 -0
  40. package/dist/negotiation/public/index.js +2 -0
  41. package/dist/opportunity/application/index.d.ts +0 -3
  42. package/dist/opportunity/application/index.js +0 -3
  43. package/dist/opportunity/application/opportunity.graph.d.ts +0 -43
  44. package/dist/opportunity/application/opportunity.graph.js +7 -113
  45. package/dist/opportunity/application/opportunity.introducer-feature.d.ts +2 -0
  46. package/dist/opportunity/application/opportunity.introducer-feature.js +4 -0
  47. package/dist/opportunity/application/opportunity.owner-approval.d.ts +118 -0
  48. package/dist/opportunity/application/opportunity.owner-approval.js +34 -0
  49. package/dist/opportunity/application/opportunity.owner-provenance.d.ts +6 -0
  50. package/dist/opportunity/application/opportunity.owner-provenance.js +28 -0
  51. package/dist/opportunity/application/opportunity.tools.d.ts +3 -3
  52. package/dist/opportunity/application/opportunity.tools.js +77 -870
  53. package/dist/opportunity/discriminator/discriminator.shadow.js +17 -10
  54. package/dist/opportunity/discriminator/discriminator.types.d.ts +2 -0
  55. package/dist/opportunity/domain/index.d.ts +0 -2
  56. package/dist/opportunity/domain/index.js +0 -2
  57. package/dist/opportunity/domain/opportunity.state.d.ts +2 -70
  58. package/dist/opportunity/domain/opportunity.state.js +2 -55
  59. package/dist/opportunity/opportunity.owner-approval.d.ts +5 -0
  60. package/dist/opportunity/opportunity.owner-approval.js +5 -0
  61. package/dist/opportunity/ports/opportunity.tools.port.d.ts +12 -1
  62. package/dist/opportunity/public/index.d.ts +3 -3
  63. package/dist/opportunity/public/index.js +2 -1
  64. package/dist/participant-agents/application/agent.tools.d.ts +1 -1
  65. package/dist/participant-agents/application/agent.tools.js +44 -6
  66. package/dist/participant-agents/application/index.d.ts +5 -3
  67. package/dist/participant-agents/application/index.js +5 -3
  68. package/dist/questions/application/question.tools.js +31 -2
  69. package/dist/runtime/foreground/composition/tool.factory.d.ts +2 -1
  70. package/dist/runtime/foreground/composition/tool.factory.js +9 -2
  71. package/dist/runtime/foreground/composition/tool.registry.d.ts +16 -2
  72. package/dist/runtime/foreground/composition/tool.registry.js +41 -38
  73. package/dist/shared/agent/activity-projection.d.ts +271 -0
  74. package/dist/shared/agent/activity-projection.js +248 -0
  75. package/dist/shared/agent/canonical-guidance.d.ts +30 -0
  76. package/dist/shared/agent/canonical-guidance.js +249 -0
  77. package/dist/shared/agent/tool.helpers.d.ts +9 -17
  78. package/dist/shared/agent/tool.runtime.js +1 -3
  79. package/dist/shared/agent/utility.tools.d.ts +11 -1
  80. package/dist/shared/agent/utility.tools.js +126 -90
  81. package/dist/shared/interfaces/auth.interface.d.ts +2 -8
  82. package/dist/shared/interfaces/chat-session.interface.js +5 -0
  83. package/dist/shared/interfaces/database.interface.d.ts +12 -5
  84. package/dist/shared/observability/request-context.d.ts +4 -40
  85. package/dist/shared/schemas/mcp-auth.schema.d.ts +87 -15
  86. package/dist/shared/schemas/mcp-auth.schema.js +49 -1
  87. package/dist/signals/application/intent.tools.js +7 -8
  88. package/package.json +10 -9
  89. package/dist/opportunity/application/opportunity.discover.d.ts +0 -215
  90. package/dist/opportunity/application/opportunity.discover.js +0 -793
  91. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.d.ts +0 -34
  92. package/dist/opportunity/application/opportunity.discovery-continuation-finalization.js +0 -74
  93. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -44
  94. package/dist/opportunity/application/opportunity.discovery-mcp-lifecycle-finalization.js +0 -92
  95. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.d.ts +0 -33
  96. package/dist/opportunity/domain/opportunity.discovery-run-coalescing.js +0 -33
  97. package/dist/opportunity/domain/opportunity.discovery.contracts.d.ts +0 -45
  98. package/dist/opportunity/domain/opportunity.discovery.contracts.js +0 -1
  99. package/dist/opportunity/opportunity.discover.d.ts +0 -5
  100. package/dist/opportunity/opportunity.discover.js +0 -5
  101. package/dist/opportunity/opportunity.discovery-continuation-finalization.d.ts +0 -5
  102. package/dist/opportunity/opportunity.discovery-continuation-finalization.js +0 -5
  103. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.d.ts +0 -5
  104. package/dist/opportunity/opportunity.discovery-mcp-lifecycle-finalization.js +0 -5
  105. package/dist/opportunity/opportunity.discovery-run-coalescing.d.ts +0 -5
  106. package/dist/opportunity/opportunity.discovery-run-coalescing.js +0 -5
  107. package/dist/opportunity/opportunity.discovery.contracts.d.ts +0 -5
  108. package/dist/opportunity/opportunity.discovery.contracts.js +0 -5
  109. package/dist/shared/interfaces/discovery-run.interface.d.ts +0 -70
  110. package/dist/shared/interfaces/discovery-run.interface.js +0 -1
@@ -624,22 +624,19 @@ export class NegotiationGraphFactory {
624
624
  });
625
625
  }
626
626
  traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `${turn.action}` });
627
- // First turn must open the negotiation (unless continuing a prior
628
- // conversation): v1 "propose"; v2 initiator "outreach". A v2 turn-0
629
- // speaker holding the counterparty seat (tie-break inheritance) is left
630
- // unforced it is responding, not opening.
631
- if (state.turnCount === 0 && !state.isContinuation) {
632
- const openingAction = version === 'v2' ? 'outreach' : 'propose';
633
- if ((version !== 'v2' || seat === 'initiator') && turn.action !== openingAction) {
634
- turnLog.warn(`Agent returned unexpected action on turn 0, forcing to ${openingAction}`, { action: turn.action });
635
- turn.action = openingAction;
636
- }
637
- }
638
- // IND-564: block a `withdraw` that comes before the initiator has opened
639
- // (no in-task `outreach`) — this would retract a message never made and
640
- // drop a spurious message into the shared thread. Exact ask_user resumes
641
- // are exempt: the successor is the SAME logical negotiation resumed after
642
- // the client answered, so post-consultation withdraw is legitimate.
627
+ // IND-564 / IND-611: the opening-withdraw guard runs BEFORE the turn-0
628
+ // opening force below. Order matters and used to be inverted: the force
629
+ // rewrote a turn-0 `withdraw` into `outreach` first, which (a) made this
630
+ // guard dead code for a v2 initiator on turn 0 and (b) sent an outreach
631
+ // whose surviving `reasoning` argued against the match to the
632
+ // counterparty. An honest turn-0 refusal is now allowed to stand and
633
+ // flows into the existing quiet `screened_out` path no message is
634
+ // persisted, so the original guard's intent (never retract a message
635
+ // that was never made) is preserved rather than weakened.
636
+ //
637
+ // Exact ask_user resumes are exempt: the successor is the SAME logical
638
+ // negotiation resumed after the client answered, so post-consultation
639
+ // withdraw is legitimate.
643
640
  if (turn.action === 'withdraw' && !state.outreachOpened && !state.continuationExecution) {
644
641
  turnLog.info('negotiation_opening_withdraw_screened_out', {
645
642
  taskId: state.taskId,
@@ -651,6 +648,21 @@ export class NegotiationGraphFactory {
651
648
  traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "screened_out: opening withdraw" });
652
649
  return { lastTurn: turn, firstTurnScreenedOut: true };
653
650
  }
651
+ // First turn must open the negotiation (unless continuing a prior
652
+ // conversation): v1 → "propose"; v2 initiator → "outreach". A v2 turn-0
653
+ // speaker holding the counterparty seat (tie-break inheritance) is left
654
+ // unforced — it is responding, not opening.
655
+ //
656
+ // A legitimate turn-0 refusal never reaches here: the opening-withdraw
657
+ // guard above already returned. What remains are genuinely malformed
658
+ // openings (a turn-0 `counter`/`question`), which are still coerced.
659
+ if (state.turnCount === 0 && !state.isContinuation) {
660
+ const openingAction = version === 'v2' ? 'outreach' : 'propose';
661
+ if ((version !== 'v2' || seat === 'initiator') && turn.action !== openingAction) {
662
+ turnLog.warn(`Agent returned unexpected action on turn 0, forcing to ${openingAction}`, { action: turn.action });
663
+ turn.action = openingAction;
664
+ }
665
+ }
654
666
  // IND-508 deterministic admission is evaluated only after the opening
655
667
  // guard but before any turn is persisted. It consumes action/role enums
656
668
  // only; free-form reasoning, messages, profiles, and evaluator inputs
@@ -1015,7 +1027,17 @@ export class NegotiationGraphFactory {
1015
1027
  // IND-564: an opening-move `withdraw` blocked before any message was
1016
1028
  // persisted is the same quiet screen-out outcome (no in-task outreach to
1017
1029
  // retract), reached from the turn node rather than the screen node.
1018
- const screenedOut = blocksNegotiationBeforeFirstTurn(state.screenDecision, state.turnCount) || state.firstTurnScreenedOut === true;
1030
+ // Two DISTINCT routes reach the same quiet `screened_out` outcome, and
1031
+ // they disagree about who authored the decision:
1032
+ // - the screen node blocked before any turn was drafted → the screen
1033
+ // decision is the reason;
1034
+ // - the acting agent refused on its opening turn (IND-611) → the
1035
+ // withdrawing TURN is the reason.
1036
+ // They are collapsed into `screenedOut` for status/lifecycle purposes but
1037
+ // must stay separate when attributing `outcome.reasoning` below.
1038
+ const blockedByScreenNode = blocksNegotiationBeforeFirstTurn(state.screenDecision, state.turnCount);
1039
+ const refusedAtOpeningTurn = state.firstTurnScreenedOut === true;
1040
+ const screenedOut = blockedByScreenNode || refusedAtOpeningTurn;
1019
1041
  const atCap = !screenedOut && (state.maxTurns ?? 0) > 0 && state.turnCount >= state.maxTurns && !isTerminalAction(lastTurn?.action);
1020
1042
  let agreedRoles = [];
1021
1043
  if (hasOpportunity && history.length >= 2) {
@@ -1033,9 +1055,19 @@ export class NegotiationGraphFactory {
1033
1055
  const outcome = {
1034
1056
  hasOpportunity,
1035
1057
  agreedRoles,
1036
- reasoning: screenedOut
1058
+ // IND-611: attribute the reasoning to whoever actually made the
1059
+ // decision. Before the turn-0 refusal path existed, `screenedOut`
1060
+ // implied the screen node, so preferring `screenDecision.reasoning`
1061
+ // was always right. It is now wrong for an opening-turn refusal taken
1062
+ // while the screen said `reach_out`: that record argues FOR the match,
1063
+ // and surfacing it as the reason the agent did NOT reach out is exactly
1064
+ // the dishonesty this work removes (IND-610 renders this string in the
1065
+ // owner-only gate-decision card). The screen-node branch is unchanged.
1066
+ reasoning: blockedByScreenNode
1037
1067
  ? (state.screenDecision?.reasoning ?? lastTurn?.assessment?.reasoning ?? "")
1038
- : (lastTurn?.assessment.reasoning ?? ""),
1068
+ : refusedAtOpeningTurn
1069
+ ? (lastTurn?.assessment?.reasoning ?? state.screenDecision?.reasoning ?? "")
1070
+ : (lastTurn?.assessment.reasoning ?? ""),
1039
1071
  turnCount: state.turnCount,
1040
1072
  ...(screenedOut
1041
1073
  ? { reason: "screened_out" }
@@ -1242,7 +1274,7 @@ export class NegotiationGraphFactory {
1242
1274
  * @returns Only candidates that produced an opportunity
1243
1275
  */
1244
1276
  export async function negotiateCandidates(negotiationGraph, sourceUser, candidates, indexContext, opts) {
1245
- const { maxTurns, traceEmitter, indexContextOverrides, timeoutMs, onCandidateResolved, trigger, initiatorUserId, resumeFromTaskId, continuationSettlementId, continuationExecution, } = opts ?? {};
1277
+ const { maxTurns, traceEmitter, indexContextOverrides, timeoutMs, onCandidateResolved, initiatorUserId, resumeFromTaskId, continuationSettlementId, continuationExecution, } = opts ?? {};
1246
1278
  // Local helper to emit events whose shape is wider than the declared
1247
1279
  // `TraceEmitter` union (mirrors the cast used in chat.agent at the relay sink
1248
1280
  // and inside turn/finalize nodes above).
@@ -1260,7 +1292,6 @@ export async function negotiateCandidates(negotiationGraph, sourceUser, candidat
1260
1292
  candidateUserId: candidate.userId,
1261
1293
  initiatorUserId: initiatorUserId ?? candidateSourceUser.id,
1262
1294
  ...(candidateName && { candidateName }),
1263
- trigger: trigger ?? "ambient",
1264
1295
  startedAt: start,
1265
1296
  });
1266
1297
  }
@@ -1347,8 +1378,7 @@ export async function negotiateCandidates(negotiationGraph, sourceUser, candidat
1347
1378
  }
1348
1379
  catch (hookErr) {
1349
1380
  // Hook failures must not sink the candidate result — the aggregate
1350
- // return is still useful, and the orchestrator branch logs its own
1351
- // failures inline.
1381
+ // return remains useful to the caller.
1352
1382
  negotiateCandidatesLog.error("onCandidateResolved hook threw", {
1353
1383
  candidateUserId: candidate.userId,
1354
1384
  error: hookErr,
@@ -7,6 +7,7 @@
7
7
  * - NegotiationGraphState and all graph state types (negotiation.state.ts)
8
8
  * - Seat-scoped protocol rules and action vocabulary (negotiation.protocol.ts)
9
9
  * - Screen-gate pure contracts: mode, decision record, block predicate (negotiation.screen.contracts.ts)
10
+ * - Negotiator stance contracts + prompt fragments (negotiation.stance.contracts.ts)
10
11
  * - Deadlock detection + bargaining prompt section (negotiation.deadlock.ts)
11
12
  * - Lifecycle narration mapper (negotiation.lifecycle-narration.ts)
12
13
  * - Task conversation-lock predicate (negotiation.task-lock-policy.ts)
@@ -30,6 +31,8 @@
30
31
  */
31
32
  export { NEGOTIATION_SCREEN_MODES, configuredScreenMode, ScreenDecisionSchema, blocksNegotiationBeforeFirstTurn, } from "./negotiation.screen.contracts.js";
32
33
  export type { NegotiationScreenMode, ScreenDecision, ScreenDecisionRecord, } from "./negotiation.screen.contracts.js";
34
+ export { NEGOTIATOR_STANCES, DEFAULT_NEGOTIATOR_STANCE, configuredNegotiatorStance, stanceAppliesValueBar, stanceQueryMatchIsNecessaryNotSufficient, stanceResolvesDeadlockByStalemate, stanceJobFraming, stanceActionRules, stanceQuerySatisfiedRule, } from "./negotiation.stance.contracts.js";
35
+ export type { NegotiatorStance } from "./negotiation.stance.contracts.js";
33
36
  export { NegotiationTurnSchema, SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, NegotiationOutcomeSchema, NegotiationGraphState, } from "./negotiation.state.js";
34
37
  export type { NegotiationTurn, NegotiationOutcome, UserNegotiationContext, SeedAssessment, NegotiationGraphLike, NegotiationMessage, } from "./negotiation.state.js";
35
38
  export { InitiatorTurnSchema, CounterpartyTurnSchema, FinalInitiatorTurnSchema, FinalCounterpartyTurnSchema, InitiatorAskUserTurnSchema, CounterpartyAskUserTurnSchema, allowedActionsFor, turnSchemaFor, isTerminalAction, isRejectLikeAction, fallbackActionFor, rejectActionFor, readProtocolVersion, configuredProtocolVersion, configuredAskUserEnabled, askUserAnswerWindowMs, ASK_USER_LOCK_SLACK_MS, DEFAULT_ASK_USER_WINDOW_MS, resolveSeat, seatViolationMessage, } from "./negotiation.protocol.js";
@@ -7,6 +7,7 @@
7
7
  * - NegotiationGraphState and all graph state types (negotiation.state.ts)
8
8
  * - Seat-scoped protocol rules and action vocabulary (negotiation.protocol.ts)
9
9
  * - Screen-gate pure contracts: mode, decision record, block predicate (negotiation.screen.contracts.ts)
10
+ * - Negotiator stance contracts + prompt fragments (negotiation.stance.contracts.ts)
10
11
  * - Deadlock detection + bargaining prompt section (negotiation.deadlock.ts)
11
12
  * - Lifecycle narration mapper (negotiation.lifecycle-narration.ts)
12
13
  * - Task conversation-lock predicate (negotiation.task-lock-policy.ts)
@@ -30,6 +31,8 @@
30
31
  */
31
32
  // ── Screen contracts ──────────────────────────────────────────────────────────
32
33
  export { NEGOTIATION_SCREEN_MODES, configuredScreenMode, ScreenDecisionSchema, blocksNegotiationBeforeFirstTurn, } from "./negotiation.screen.contracts.js";
34
+ // ── Stance contracts (IND-611) ────────────────────────────────────────────────
35
+ export { NEGOTIATOR_STANCES, DEFAULT_NEGOTIATOR_STANCE, configuredNegotiatorStance, stanceAppliesValueBar, stanceQueryMatchIsNecessaryNotSufficient, stanceResolvesDeadlockByStalemate, stanceJobFraming, stanceActionRules, stanceQuerySatisfiedRule, } from "./negotiation.stance.contracts.js";
33
36
  // ── Graph state and DTOs ──────────────────────────────────────────────────────
34
37
  export { NegotiationTurnSchema, SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, NegotiationOutcomeSchema, NegotiationGraphState, } from "./negotiation.state.js";
35
38
  // ── Protocol rules ────────────────────────────────────────────────────────────
@@ -19,6 +19,7 @@
19
19
  * - **Fail-open**: any detection error means "no deadlock" — advisory
20
20
  * infrastructure never blocks a negotiation.
21
21
  */
22
+ import { type NegotiatorStance } from "./negotiation.stance.contracts.js";
22
23
  export type { DeadlockShiftRecord } from "./negotiation.deadlock.contracts.js";
23
24
  /**
24
25
  * Whether the deadlock→bargaining mode shift is enabled, from the
@@ -66,16 +67,21 @@ export declare function assessDeadlock(history: ReadonlyArray<{
66
67
  action?: string;
67
68
  }>, threshold?: number): DeadlockAssessment;
68
69
  /**
69
- * Renders the bargaining-stance prompt section. Returns the empty string when
70
+ * Renders the deadlock-stance prompt section. Returns the empty string when
70
71
  * the shift is not active, so the rendered system prompt is byte-identical to
71
72
  * the legacy build on every non-shifted turn (mirrors
72
73
  * `renderNegotiatorMemorySection`). The `ask_user` escalation line renders
73
74
  * only when the caller already legally holds the action (`canAskUser`) — the
74
75
  * shift never invents a locution.
76
+ *
77
+ * IND-611: `stance` selects which resolution the shift drafts toward —
78
+ * bargaining (`advocate`/`evaluator`, the default and today's behavior) or
79
+ * stalemate (`skeptic`). Omitted → bargaining, byte-identical to before.
75
80
  */
76
81
  export declare function renderBargainingShiftSection(input: {
77
82
  active: boolean;
78
83
  userName: string;
79
84
  canAskUser: boolean;
80
85
  consecutiveNonConvergent: number;
86
+ stance?: NegotiatorStance;
81
87
  }): string;
@@ -1,3 +1,25 @@
1
+ /**
2
+ * Deadlock detection + persuasion→bargaining mode shift (IND-428, backlog item 6).
3
+ *
4
+ * Grounding: Wells & Reed (2006), *Knowing When to Bargain* — a persuasion
5
+ * dialogue (arguing the merits) that reaches a stalemate may execute a *legal
6
+ * shift* into a negotiation dialogue (offering concessions). See
7
+ * `docs/design/negotiation-dialogue-game.md` for the formal framing of the
8
+ * turn protocol as a dialogue game.
9
+ *
10
+ * Design constraints (hard):
11
+ * - **Deterministic**: deadlock is decided by pure inspection of the persisted
12
+ * turn history — never by an LLM.
13
+ * - **Stance, not rules**: a detected deadlock changes the system agent's
14
+ * *drafting stance* only. Locutions, seat vocabularies (`allowedActionsFor`),
15
+ * termination, and turn-cap semantics are untouched.
16
+ * - **Default-off**: gated on `NEGOTIATION_DEADLOCK_SHIFT_ENABLED === "true"`
17
+ * (strict literal) and applied only to v2 negotiations, checked alongside the
18
+ * protocol-version plumbing. When off, the legacy path is byte-identical.
19
+ * - **Fail-open**: any detection error means "no deadlock" — advisory
20
+ * infrastructure never blocks a negotiation.
21
+ */
22
+ import { stanceResolvesDeadlockByStalemate } from "./negotiation.stance.contracts.js";
1
23
  // ─── Configuration ───────────────────────────────────────────────────────────
2
24
  /**
3
25
  * Whether the deadlock→bargaining mode shift is enabled, from the
@@ -86,16 +108,41 @@ This shift changes your stance only — your available actions are unchanged.`;
86
108
  const BARGAINING_ASK_USER_ESCALATION = `
87
109
  - If a concession would require {userName}'s own input or permission (budget, availability, private details), escalate with "ask_user" instead of guessing.`;
88
110
  /**
89
- * Renders the bargaining-stance prompt section. Returns the empty string when
111
+ * Skeptic-stance deadlock resolution (IND-611). Under `NEGOTIATOR_STANCE=skeptic`
112
+ * a stalemate resolves as a stalemate rather than by bargaining: the designed
113
+ * outcome of persistent disagreement stops being "a smaller match" and becomes
114
+ * "possibly no match". Same trigger, same threshold, same actions — only the
115
+ * drafting stance differs. `advocate`/`evaluator` keep the bargaining section
116
+ * byte-identical.
117
+ */
118
+ const STALEMATE_SHIFT_SECTION = `
119
+
120
+ DEADLOCK — THE MERITS ARE EXHAUSTED. The last {consecutive} turns were counters/questions without convergence: the merits have been argued and restating them will not move the other side. For this turn, change stance:
121
+ - Do NOT re-argue fit or repeat points already made.
122
+ - Do NOT buy agreement with a concession or a reduced scope. A shrunken version of a match that could not be agreed on its merits is usually worth less of {userName}'s attention than the full one was, not more.
123
+ - Name the ONE specific thing that would genuinely change your assessment. If it has not appeared after this many turns, it is unlikely to.
124
+ - Absent that, conclude decisively with a terminal action from your allowed set. An unresolved disagreement is an acceptable outcome; a match made to end the disagreement is not.
125
+ This shift changes your stance only — your available actions are unchanged.`;
126
+ /**
127
+ * Renders the deadlock-stance prompt section. Returns the empty string when
90
128
  * the shift is not active, so the rendered system prompt is byte-identical to
91
129
  * the legacy build on every non-shifted turn (mirrors
92
130
  * `renderNegotiatorMemorySection`). The `ask_user` escalation line renders
93
131
  * only when the caller already legally holds the action (`canAskUser`) — the
94
132
  * shift never invents a locution.
133
+ *
134
+ * IND-611: `stance` selects which resolution the shift drafts toward —
135
+ * bargaining (`advocate`/`evaluator`, the default and today's behavior) or
136
+ * stalemate (`skeptic`). Omitted → bargaining, byte-identical to before.
95
137
  */
96
138
  export function renderBargainingShiftSection(input) {
97
139
  if (!input.active)
98
140
  return "";
141
+ if (input.stance && stanceResolvesDeadlockByStalemate(input.stance)) {
142
+ return STALEMATE_SHIFT_SECTION
143
+ .replace("{consecutive}", String(input.consecutiveNonConvergent))
144
+ .replace(/{userName}/g, input.userName);
145
+ }
99
146
  return BARGAINING_SHIFT_SECTION
100
147
  .replace("{consecutive}", String(input.consecutiveNonConvergent))
101
148
  .replace("{askUserEscalation}", input.canAskUser ? BARGAINING_ASK_USER_ESCALATION : "")
@@ -0,0 +1,85 @@
1
+ /**
2
+ * negotiation/domain — negotiator stance contracts (IND-611).
3
+ *
4
+ * The acting negotiator prompt is structurally biased toward producing matches
5
+ * rather than finding valuable ones: it frames the job as advocacy, treats a
6
+ * discovery-query match as a mandate to connect, and carries no
7
+ * opportunity-cost term — the only decline bar is the purely negative "does not
8
+ * serve {userName}'s needs", so absence of harm reads as grounds to accept.
9
+ *
10
+ * `NEGOTIATOR_STANCE` makes that stance configurable instead of hard-coded:
11
+ *
12
+ * | stance | framing | value bar | query rule | deadlock |
13
+ * |-------------|--------------------------------------|------------------|-------------------------|-----------|
14
+ * | `advocate` | argue the case (today) | none | mandate (today) | bargain |
15
+ * | `evaluator` | assess first, advocate if it survives | opportunity-cost | necessary-not-sufficient| bargain |
16
+ * | `skeptic` | + "most matches are not worth making" | opportunity-cost | necessary-not-sufficient| stalemate |
17
+ *
18
+ * Design constraints (hard):
19
+ * - **`advocate` is byte-identical.** Every fragment below is additive and
20
+ * gated; under `advocate` each renderer returns the exact legacy string, so
21
+ * the rendered prompt is byte-for-byte the pre-IND-611 build. Existing prompt
22
+ * specs are the guard.
23
+ * - **Prompt-only.** The stance changes drafting stance and nothing else:
24
+ * no seat vocabulary change (`allowedActionsFor`), no schema change, no graph
25
+ * routing change. In particular the continuation-screen bypass at
26
+ * `negotiation.graph.ts` (`!state.continuationExecution`) is identical under
27
+ * all three stances — deliberately deferred so evals isolate wording as the
28
+ * single variable.
29
+ * - **Domain layer.** Placed here (not in `application/`) so both the
30
+ * application-layer agent and the domain-layer deadlock renderer can read the
31
+ * stance without a domain → application cycle.
32
+ * - **Fail-safe default.** Unset or unrecognized falls back to `advocate`
33
+ * (today's behavior), same operational pattern as `configuredScreenMode()`.
34
+ *
35
+ * Fragments deliberately never contain the literal `ask_user` or a quoted
36
+ * `"withdraw"`: they render into every seat and protocol version, and the seat
37
+ * specs pin that those tokens appear only where the seat legally holds them.
38
+ */
39
+ export declare const NEGOTIATOR_STANCES: readonly ["advocate", "evaluator", "skeptic"];
40
+ export type NegotiatorStance = (typeof NEGOTIATOR_STANCES)[number];
41
+ export declare const DEFAULT_NEGOTIATOR_STANCE: NegotiatorStance;
42
+ /**
43
+ * Resolve the negotiator stance from `NEGOTIATOR_STANCE`.
44
+ *
45
+ * Defaults to `advocate` when unset or unrecognized — the stance shift is an
46
+ * explicit opt-in flip (same operational pattern as `NEGOTIATION_SCREEN_MODE` /
47
+ * `NEGOTIATION_PROTOCOL_VERSION`): the code ships dark, the environment turns
48
+ * it on.
49
+ */
50
+ export declare function configuredNegotiatorStance(): NegotiatorStance;
51
+ /** Whether this stance applies the opportunity-cost value bar. */
52
+ export declare function stanceAppliesValueBar(stance: NegotiatorStance): boolean;
53
+ /**
54
+ * Whether this stance treats a discovery-query match as a precondition for
55
+ * continuing to evaluate rather than as a mandate to connect.
56
+ */
57
+ export declare function stanceQueryMatchIsNecessaryNotSufficient(stance: NegotiatorStance): boolean;
58
+ /** Whether a detected deadlock resolves by stalemate rather than bargaining. */
59
+ export declare function stanceResolvesDeadlockByStalemate(stance: NegotiatorStance): boolean;
60
+ /**
61
+ * The job-framing sentence for a stance. Under `advocate` this is the exact
62
+ * legacy sentence, so the rendered prompt is byte-identical.
63
+ *
64
+ * `{userName}` placeholders are left intact for the caller's existing global
65
+ * replace.
66
+ */
67
+ export declare function stanceJobFraming(stance: NegotiatorStance): string;
68
+ /**
69
+ * Extra action-rule lines contributed by the stance, appended after the seat's
70
+ * own rules. Empty under `advocate` → byte-identical.
71
+ */
72
+ export declare function stanceActionRules(stance: NegotiatorStance): string;
73
+ /**
74
+ * The discovery-query satisfaction rule.
75
+ *
76
+ * `advocate` keeps today's mandate wording verbatim ("PROPOSE or ACCEPT the
77
+ * connection…"), which converts a filter into an instruction to connect.
78
+ * `evaluator`/`skeptic` make query satisfaction necessary-but-not-sufficient: a
79
+ * precondition for continuing to evaluate, never itself a reason to connect.
80
+ *
81
+ * Names are interpolated eagerly here: this fragment is spliced into the system
82
+ * prompt *after* the caller's global `{userName}` substitution has already run,
83
+ * so a placeholder would survive into the rendered prompt.
84
+ */
85
+ export declare function stanceQuerySatisfiedRule(stance: NegotiatorStance, otherName: string, userName: string): string;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * negotiation/domain — negotiator stance contracts (IND-611).
3
+ *
4
+ * The acting negotiator prompt is structurally biased toward producing matches
5
+ * rather than finding valuable ones: it frames the job as advocacy, treats a
6
+ * discovery-query match as a mandate to connect, and carries no
7
+ * opportunity-cost term — the only decline bar is the purely negative "does not
8
+ * serve {userName}'s needs", so absence of harm reads as grounds to accept.
9
+ *
10
+ * `NEGOTIATOR_STANCE` makes that stance configurable instead of hard-coded:
11
+ *
12
+ * | stance | framing | value bar | query rule | deadlock |
13
+ * |-------------|--------------------------------------|------------------|-------------------------|-----------|
14
+ * | `advocate` | argue the case (today) | none | mandate (today) | bargain |
15
+ * | `evaluator` | assess first, advocate if it survives | opportunity-cost | necessary-not-sufficient| bargain |
16
+ * | `skeptic` | + "most matches are not worth making" | opportunity-cost | necessary-not-sufficient| stalemate |
17
+ *
18
+ * Design constraints (hard):
19
+ * - **`advocate` is byte-identical.** Every fragment below is additive and
20
+ * gated; under `advocate` each renderer returns the exact legacy string, so
21
+ * the rendered prompt is byte-for-byte the pre-IND-611 build. Existing prompt
22
+ * specs are the guard.
23
+ * - **Prompt-only.** The stance changes drafting stance and nothing else:
24
+ * no seat vocabulary change (`allowedActionsFor`), no schema change, no graph
25
+ * routing change. In particular the continuation-screen bypass at
26
+ * `negotiation.graph.ts` (`!state.continuationExecution`) is identical under
27
+ * all three stances — deliberately deferred so evals isolate wording as the
28
+ * single variable.
29
+ * - **Domain layer.** Placed here (not in `application/`) so both the
30
+ * application-layer agent and the domain-layer deadlock renderer can read the
31
+ * stance without a domain → application cycle.
32
+ * - **Fail-safe default.** Unset or unrecognized falls back to `advocate`
33
+ * (today's behavior), same operational pattern as `configuredScreenMode()`.
34
+ *
35
+ * Fragments deliberately never contain the literal `ask_user` or a quoted
36
+ * `"withdraw"`: they render into every seat and protocol version, and the seat
37
+ * specs pin that those tokens appear only where the seat legally holds them.
38
+ */
39
+ export const NEGOTIATOR_STANCES = ["advocate", "evaluator", "skeptic"];
40
+ export const DEFAULT_NEGOTIATOR_STANCE = "advocate";
41
+ /**
42
+ * Resolve the negotiator stance from `NEGOTIATOR_STANCE`.
43
+ *
44
+ * Defaults to `advocate` when unset or unrecognized — the stance shift is an
45
+ * explicit opt-in flip (same operational pattern as `NEGOTIATION_SCREEN_MODE` /
46
+ * `NEGOTIATION_PROTOCOL_VERSION`): the code ships dark, the environment turns
47
+ * it on.
48
+ */
49
+ export function configuredNegotiatorStance() {
50
+ const raw = process.env.NEGOTIATOR_STANCE;
51
+ if (raw === "advocate" || raw === "evaluator" || raw === "skeptic")
52
+ return raw;
53
+ return DEFAULT_NEGOTIATOR_STANCE;
54
+ }
55
+ /** Whether this stance applies the opportunity-cost value bar. */
56
+ export function stanceAppliesValueBar(stance) {
57
+ return stance !== "advocate";
58
+ }
59
+ /**
60
+ * Whether this stance treats a discovery-query match as a precondition for
61
+ * continuing to evaluate rather than as a mandate to connect.
62
+ */
63
+ export function stanceQueryMatchIsNecessaryNotSufficient(stance) {
64
+ return stance !== "advocate";
65
+ }
66
+ /** Whether a detected deadlock resolves by stalemate rather than bargaining. */
67
+ export function stanceResolvesDeadlockByStalemate(stance) {
68
+ return stance === "skeptic";
69
+ }
70
+ // ─── Prompt fragments ────────────────────────────────────────────────────────
71
+ /**
72
+ * The `advocate` job framing — byte-identical to the pre-IND-611 sentence that
73
+ * followed "Your job: Evaluate whether this connection genuinely serves
74
+ * {userName}'s interests given their role." in the system prompt.
75
+ */
76
+ const ADVOCATE_FRAMING = `Argue their case honestly — acknowledge weaknesses, but advocate for genuine fit.`;
77
+ /**
78
+ * `evaluator`: assessment precedes advocacy. Advocacy is still available — it is
79
+ * conditioned on the match surviving an honest judgment first.
80
+ */
81
+ const EVALUATOR_FRAMING = `Assess before you advocate: first form an honest judgment about whether this connection is actually worth making for {userName}. Advocate only for a match that survives that judgment, and say so plainly when one does not.`;
82
+ /**
83
+ * `skeptic`: the evaluator framing plus an explicit prior. Borrows the finite-
84
+ * attention framing already proven in the outreach gate prompt
85
+ * (`negotiation.screen.ts`) — {userName}'s name and attention are spent on
86
+ * every connection made for them.
87
+ */
88
+ const SKEPTIC_FRAMING = `${EVALUATOR_FRAMING} Start from the prior that most candidate matches are NOT worth making: {userName}'s attention is finite and a mediocre connection costs them more than no connection. The burden is on the match to earn their time, not on you to find a way to say yes.`;
89
+ /**
90
+ * The job-framing sentence for a stance. Under `advocate` this is the exact
91
+ * legacy sentence, so the rendered prompt is byte-identical.
92
+ *
93
+ * `{userName}` placeholders are left intact for the caller's existing global
94
+ * replace.
95
+ */
96
+ export function stanceJobFraming(stance) {
97
+ switch (stance) {
98
+ case "evaluator":
99
+ return EVALUATOR_FRAMING;
100
+ case "skeptic":
101
+ return SKEPTIC_FRAMING;
102
+ default:
103
+ return ADVOCATE_FRAMING;
104
+ }
105
+ }
106
+ /**
107
+ * The opportunity-cost value bar, appended as an extra action rule.
108
+ *
109
+ * Today the only decline bar in the prompt is "does not serve {userName}'s
110
+ * needs" — a purely negative test, so absence of harm reads as grounds to
111
+ * proceed. This adds the missing positive bar without touching any seat's
112
+ * action vocabulary.
113
+ */
114
+ const VALUE_BAR_RULE = `
115
+ - OPPORTUNITY COST: {userName}'s attention is finite and their name is spent on every connection made on their behalf. The bar is "worth that spend", not "does no harm" — absence of a downside is NOT a reason to proceed. Ask what {userName} gives up by spending this attention here instead of on a better match, and say no when the answer is "too much".`;
116
+ /**
117
+ * Extra action-rule lines contributed by the stance, appended after the seat's
118
+ * own rules. Empty under `advocate` → byte-identical.
119
+ */
120
+ export function stanceActionRules(stance) {
121
+ return stanceAppliesValueBar(stance) ? VALUE_BAR_RULE : "";
122
+ }
123
+ /**
124
+ * The discovery-query satisfaction rule.
125
+ *
126
+ * `advocate` keeps today's mandate wording verbatim ("PROPOSE or ACCEPT the
127
+ * connection…"), which converts a filter into an instruction to connect.
128
+ * `evaluator`/`skeptic` make query satisfaction necessary-but-not-sufficient: a
129
+ * precondition for continuing to evaluate, never itself a reason to connect.
130
+ *
131
+ * Names are interpolated eagerly here: this fragment is spliced into the system
132
+ * prompt *after* the caller's global `{userName}` substitution has already run,
133
+ * so a placeholder would survive into the rendered prompt.
134
+ */
135
+ export function stanceQuerySatisfiedRule(stance, otherName, userName) {
136
+ if (!stanceQueryMatchIsNecessaryNotSufficient(stance)) {
137
+ return `- If ${otherName} DOES satisfy the query: PROPOSE or ACCEPT the connection and evaluate fit normally using intents and profile data.`;
138
+ }
139
+ return `- If ${otherName} DOES satisfy the query: satisfying the query is a PRECONDITION for continuing to evaluate, NOT a reason to connect. Keep evaluating fit on intents and profile data, and decline when the connection would not be worth ${userName}'s attention.`;
140
+ }
@@ -401,9 +401,8 @@ export declare const NegotiationGraphState: import("@langchain/langgraph").Annot
401
401
  turnCount: import("@langchain/langgraph").BaseChannel<number, number | import("@langchain/langgraph").OverwriteValue<number>, unknown>;
402
402
  maxTurns: import("@langchain/langgraph").BaseChannel<number | undefined, number | import("@langchain/langgraph").OverwriteValue<number | undefined> | undefined, unknown>;
403
403
  /**
404
- * Park-window budget in milliseconds. Ambient callers pass `AMBIENT_PARK_WINDOW_MS`
405
- * (5 minutes); orchestrator callers pass a shorter window. This annotation default
406
- * is a safety net for any caller that omits the field — keep it aligned with
404
+ * Park-window budget in milliseconds. The annotation default is a safety net
405
+ * for any caller that omits the field keep it aligned with
407
406
  * `AMBIENT_PARK_WINDOW_MS` in packages/protocol/src/negotiation/negotiation.tools.ts.
408
407
  * Inlined rather than imported to avoid a state↔tools cycle.
409
408
  */
@@ -237,9 +237,8 @@ export const NegotiationGraphState = Annotation.Root({
237
237
  default: () => undefined,
238
238
  }),
239
239
  /**
240
- * Park-window budget in milliseconds. Ambient callers pass `AMBIENT_PARK_WINDOW_MS`
241
- * (5 minutes); orchestrator callers pass a shorter window. This annotation default
242
- * is a safety net for any caller that omits the field — keep it aligned with
240
+ * Park-window budget in milliseconds. The annotation default is a safety net
241
+ * for any caller that omits the field keep it aligned with
243
242
  * `AMBIENT_PARK_WINDOW_MS` in packages/protocol/src/negotiation/negotiation.tools.ts.
244
243
  * Inlined rather than imported to avoid a state↔tools cycle.
245
244
  */
@@ -33,6 +33,8 @@ export { NegotiationTurnSchema, SystemNegotiationTurnSchema, FinalNegotiationTur
33
33
  export type { NegotiationTurn, NegotiationOutcome, UserNegotiationContext, SeedAssessment, NegotiationGraphLike, NegotiationMessage, } from "../domain/index.js";
34
34
  export { NEGOTIATION_SCREEN_MODES, configuredScreenMode, ScreenDecisionSchema, blocksNegotiationBeforeFirstTurn, } from "../domain/index.js";
35
35
  export type { NegotiationScreenMode, ScreenDecision, ScreenDecisionRecord, } from "../domain/index.js";
36
+ export { NEGOTIATOR_STANCES, DEFAULT_NEGOTIATOR_STANCE, configuredNegotiatorStance, stanceAppliesValueBar, stanceQueryMatchIsNecessaryNotSufficient, stanceResolvesDeadlockByStalemate, stanceJobFraming, stanceActionRules, stanceQuerySatisfiedRule, } from "../domain/index.js";
37
+ export type { NegotiatorStance } from "../domain/index.js";
36
38
  export { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold, renderBargainingShiftSection, DEFAULT_DEADLOCK_THRESHOLD, MIN_DEADLOCK_THRESHOLD, } from "../domain/index.js";
37
39
  export type { DeadlockAssessment, DeadlockShiftRecord, } from "../domain/index.js";
38
40
  export { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode, } from "../domain/index.js";
@@ -34,6 +34,8 @@ export { allowedActionsFor, isTerminalAction, isRejectLikeAction, readProtocolVe
34
34
  export { NegotiationTurnSchema, SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, NegotiationOutcomeSchema, NegotiationGraphState, } from "../domain/index.js";
35
35
  // ── Domain: screen contracts ──────────────────────────────────────────────────
36
36
  export { NEGOTIATION_SCREEN_MODES, configuredScreenMode, ScreenDecisionSchema, blocksNegotiationBeforeFirstTurn, } from "../domain/index.js";
37
+ // ── Domain: stance contracts (IND-611) ───────────────────────────────────────
38
+ export { NEGOTIATOR_STANCES, DEFAULT_NEGOTIATOR_STANCE, configuredNegotiatorStance, stanceAppliesValueBar, stanceQueryMatchIsNecessaryNotSufficient, stanceResolvesDeadlockByStalemate, stanceJobFraming, stanceActionRules, stanceQuerySatisfiedRule, } from "../domain/index.js";
37
39
  // ── Domain: deadlock ──────────────────────────────────────────────────────────
38
40
  export { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold, renderBargainingShiftSection, DEFAULT_DEADLOCK_THRESHOLD, MIN_DEADLOCK_THRESHOLD, } from "../domain/index.js";
39
41
  // ── Domain: consultation policy ───────────────────────────────────────────────
@@ -42,10 +42,7 @@ export * from "./opportunity.presenter.js";
42
42
  export * from "./opportunity.introducer.js";
43
43
  export * from "./opportunity.existing-negotiation.js";
44
44
  export * from "./opportunity.graph.js";
45
- export * from "./opportunity.discover.js";
46
45
  export * from "./opportunity.card-presentation.js";
47
- export * from "./opportunity.discovery-continuation-finalization.js";
48
- export * from "./opportunity.discovery-mcp-lifecycle-finalization.js";
49
46
  export * from "./opportunity.discovery-negotiation-summary.js";
50
47
  export * from "./negotiation-context.loader.js";
51
48
  export * from "./opportunity.feed-selection.js";
@@ -43,10 +43,7 @@ export * from "./opportunity.presenter.js";
43
43
  export * from "./opportunity.introducer.js";
44
44
  export * from "./opportunity.existing-negotiation.js";
45
45
  export * from "./opportunity.graph.js";
46
- export * from "./opportunity.discover.js";
47
46
  export * from "./opportunity.card-presentation.js";
48
- export * from "./opportunity.discovery-continuation-finalization.js";
49
- export * from "./opportunity.discovery-mcp-lifecycle-finalization.js";
50
47
  export * from "./opportunity.discovery-negotiation-summary.js";
51
48
  export * from "./negotiation-context.loader.js";
52
49
  export * from "./opportunity.feed-selection.js";