ultimate-pi 0.16.0 → 0.18.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 (137) hide show
  1. package/.agents/skills/harness-context/SKILL.md +13 -6
  2. package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
  3. package/.agents/skills/harness-eval/SKILL.md +6 -21
  4. package/.agents/skills/harness-governor/SKILL.md +4 -3
  5. package/.agents/skills/harness-orchestration/SKILL.md +39 -51
  6. package/.agents/skills/harness-plan/SKILL.md +23 -12
  7. package/.agents/skills/harness-review/SKILL.md +52 -0
  8. package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
  9. package/.agents/skills/harness-steer/SKILL.md +14 -0
  10. package/.pi/agents/harness/adversary.md +3 -10
  11. package/.pi/agents/harness/evaluator.md +3 -12
  12. package/.pi/agents/harness/executor.md +12 -14
  13. package/.pi/agents/harness/planning/decompose.md +7 -4
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  15. package/.pi/agents/harness/planning/hypothesis.md +4 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
  17. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  18. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  19. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  20. package/.pi/agents/harness/planning/planning-context.md +48 -0
  21. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  22. package/.pi/agents/harness/planning/scout-graphify.md +3 -1
  23. package/.pi/agents/harness/planning/scout-semantic.md +3 -1
  24. package/.pi/agents/harness/planning/scout-structure.md +3 -1
  25. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  26. package/.pi/agents/harness/sentrux-steward.md +51 -0
  27. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  28. package/.pi/extensions/harness-debate-tools.ts +12 -3
  29. package/.pi/extensions/harness-live-widget.ts +27 -1
  30. package/.pi/extensions/harness-plan-approval.ts +62 -56
  31. package/.pi/extensions/harness-run-context.ts +553 -84
  32. package/.pi/extensions/harness-subagent-submit.ts +43 -33
  33. package/.pi/extensions/harness-telemetry.ts +29 -4
  34. package/.pi/extensions/lib/debate-bus-core.ts +15 -9
  35. package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
  36. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  37. package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
  38. package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
  39. package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
  40. package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
  41. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  42. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
  43. package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
  44. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  45. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  46. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  47. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  48. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  49. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  50. package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
  51. package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
  52. package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
  53. package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
  54. package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
  55. package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
  56. package/.pi/extensions/lib/plan-messenger.ts +4 -0
  57. package/.pi/extensions/lib/plan-review-gate.ts +59 -0
  58. package/.pi/extensions/lib/posthog-client.ts +76 -0
  59. package/.pi/extensions/policy-gate.ts +24 -19
  60. package/.pi/extensions/trace-recorder.ts +1 -0
  61. package/.pi/harness/agents.manifest.json +24 -16
  62. package/.pi/harness/corpus/cron.example +8 -0
  63. package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
  64. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  65. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  66. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  67. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  68. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
  69. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  70. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  71. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  72. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  73. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  74. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  75. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  76. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  77. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  78. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  79. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
  80. package/.pi/harness/docs/adrs/README.md +10 -0
  81. package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
  82. package/.pi/harness/docs/practice-map.md +110 -0
  83. package/.pi/harness/env.harness.template +5 -3
  84. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
  85. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
  86. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
  87. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
  88. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
  89. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  90. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
  91. package/.pi/harness/specs/README.md +1 -1
  92. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  93. package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
  94. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  95. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  96. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  97. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  98. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  99. package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
  100. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  101. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  102. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  103. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  104. package/.pi/harness/specs/steer-state.schema.json +20 -0
  105. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  106. package/.pi/lib/harness-repair-brief.ts +145 -0
  107. package/.pi/lib/harness-run-context.ts +591 -32
  108. package/.pi/lib/harness-ui-state.ts +87 -9
  109. package/.pi/model-router.example.json +13 -4
  110. package/.pi/prompts/harness-auto.md +9 -9
  111. package/.pi/prompts/harness-critic.md +3 -30
  112. package/.pi/prompts/harness-eval.md +4 -37
  113. package/.pi/prompts/harness-plan.md +139 -57
  114. package/.pi/prompts/harness-review.md +150 -15
  115. package/.pi/prompts/harness-run.md +62 -10
  116. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  117. package/.pi/prompts/harness-setup.md +4 -4
  118. package/.pi/prompts/harness-steer.md +30 -0
  119. package/.pi/scripts/graphify-kb-updater.mjs +358 -0
  120. package/.pi/scripts/harness-generate-model-router.mjs +118 -36
  121. package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
  122. package/.pi/scripts/harness-sync-model-router.mjs +15 -2
  123. package/.pi/scripts/harness-verify.mjs +51 -6
  124. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  125. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +22 -0
  128. package/package.json +5 -4
  129. package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
  130. package/vendor/pi-model-router/extensions/commands.ts +4 -4
  131. package/vendor/pi-model-router/extensions/index.ts +21 -0
  132. package/vendor/pi-model-router/extensions/provider.ts +130 -79
  133. package/vendor/pi-model-router/extensions/routing.ts +148 -0
  134. package/vendor/pi-model-router/extensions/state.ts +3 -0
  135. package/vendor/pi-model-router/extensions/types.ts +9 -0
  136. package/vendor/pi-model-router/extensions/ui.ts +16 -2
  137. package/.pi/prompts/git-sync.md +0 -124
@@ -19,15 +19,21 @@ import type {
19
19
  RouterTier,
20
20
  RouterPinByProfile,
21
21
  RouterThinkingByProfile,
22
+ SessionLock,
22
23
  } from './types.js';
23
24
  import { profileNames, parseCanonicalModelRef, ROUTER_TIERS } from './config.js';
24
25
  import {
25
26
  phaseForTier,
26
27
  buildRoutingDecision,
27
28
  decideRouting,
29
+ decideSessionLock,
28
30
  runClassifier,
29
31
  extractTextFromContent,
30
32
  hasImageAttachment,
33
+ buildSessionLockContext,
34
+ sessionLockToRoutingDecision,
35
+ routingDecisionToSessionLock,
36
+ applyThinkingToDecision,
31
37
  } from './routing.js';
32
38
 
33
39
  export const createErrorMessage = (
@@ -138,6 +144,8 @@ export const registerRouterProvider = (
138
144
  readonly thinkingByProfile: RouterThinkingByProfile;
139
145
  readonly pinnedTierByProfile: RouterPinByProfile;
140
146
  accumulatedCost: number;
147
+ get sessionLock(): SessionLock | undefined;
148
+ set sessionLock(v: SessionLock | undefined);
141
149
  },
142
150
  actions: {
143
151
  persistState: () => void;
@@ -217,26 +225,111 @@ export const registerRouterProvider = (
217
225
  state.routerEnabled = true;
218
226
 
219
227
  const pinnedTier = state.pinnedTierByProfile[model.id];
228
+ const thinkingOverrides = state.thinkingByProfile[model.id];
220
229
  const isBudgetExceeded =
221
230
  state.currentConfig.maxSessionBudget !== undefined &&
222
231
  state.accumulatedCost >= state.currentConfig.maxSessionBudget;
223
232
 
224
- let decision: RoutingDecision = decideRouting(
233
+ const checkModelSupportsImage = (modelRef: string) => {
234
+ try {
235
+ const { provider, modelId } = parseCanonicalModelRef(modelRef);
236
+ const m = state.currentModelRegistry?.find(provider, modelId);
237
+ return m?.input?.includes('image') ?? false;
238
+ } catch {
239
+ return false;
240
+ }
241
+ };
242
+
243
+ const pickVisionCapableLock = (
244
+ lockDecision: RoutingDecision,
245
+ ): RoutingDecision => {
246
+ if (!hasImageAttachment(context)) return lockDecision;
247
+ const candidates = [
248
+ lockDecision.targetLabel,
249
+ ...(profile[lockDecision.tier].fallbacks ?? []),
250
+ ];
251
+ if (candidates.some(checkModelSupportsImage)) return lockDecision;
252
+ for (const tier of ROUTER_TIERS) {
253
+ const refs = [profile[tier].model, ...(profile[tier].fallbacks ?? [])];
254
+ const visionRef = refs.find(checkModelSupportsImage);
255
+ if (visionRef) {
256
+ const { provider, modelId } = parseCanonicalModelRef(visionRef);
257
+ return {
258
+ ...lockDecision,
259
+ tier,
260
+ targetLabel: visionRef,
261
+ targetProvider: provider,
262
+ targetModelId: modelId,
263
+ reasoning: `${lockDecision.reasoning} | Vision-capable model for image input.`,
264
+ };
265
+ }
266
+ }
267
+ return lockDecision;
268
+ };
269
+
270
+ let lock = state.sessionLock;
271
+ if (!lock || lock.profile !== model.id) {
272
+ const lockContext = buildSessionLockContext(context);
273
+ let lockDecision = decideSessionLock(
274
+ lockContext,
275
+ model.id,
276
+ profile,
277
+ pinnedTier,
278
+ thinkingOverrides,
279
+ state.currentConfig.phaseBias,
280
+ state.currentConfig.rules,
281
+ );
282
+
283
+ if (
284
+ state.currentConfig.classifierModel &&
285
+ !pinnedTier &&
286
+ !lockDecision.isRuleMatched
287
+ ) {
288
+ const classifierResult = await runClassifier(
289
+ state.currentConfig.classifierModel,
290
+ state.currentModelRegistry,
291
+ lockContext,
292
+ undefined,
293
+ );
294
+ if (classifierResult) {
295
+ lockDecision = buildRoutingDecision(
296
+ model.id,
297
+ profile,
298
+ classifierResult.tier,
299
+ phaseForTier(classifierResult.tier),
300
+ `Session lock classifier: ${classifierResult.reasoning}`,
301
+ thinkingOverrides,
302
+ true,
303
+ );
304
+ }
305
+ }
306
+
307
+ lockDecision = pickVisionCapableLock(lockDecision);
308
+ lock = routingDecisionToSessionLock(lockDecision);
309
+ state.sessionLock = lock;
310
+ }
311
+
312
+ const lockedBase = sessionLockToRoutingDecision(
313
+ lock,
314
+ profile,
315
+ thinkingOverrides,
316
+ );
317
+
318
+ let thinkingDecision: RoutingDecision = decideRouting(
225
319
  context,
226
320
  model.id,
227
321
  profile,
228
322
  state.lastDecision,
229
323
  pinnedTier,
230
- state.thinkingByProfile[model.id],
324
+ thinkingOverrides,
231
325
  state.currentConfig.phaseBias,
232
326
  state.currentConfig.rules,
233
327
  isBudgetExceeded,
234
328
  );
235
329
 
236
- // Optional Context Trigger Upgrade
237
330
  if (
238
331
  state.currentConfig.largeContextThreshold &&
239
- decision.tier !== 'high' &&
332
+ thinkingDecision.tier !== 'high' &&
240
333
  state.lastExtensionContext
241
334
  ) {
242
335
  try {
@@ -245,28 +338,24 @@ export const registerRouterProvider = (
245
338
  usage?.tokens &&
246
339
  usage.tokens > state.currentConfig.largeContextThreshold
247
340
  ) {
248
- decision = buildRoutingDecision(
249
- model.id,
250
- profile,
251
- 'high',
252
- 'planning',
253
- `Context usage (${usage.tokens}) exceeds threshold (${state.currentConfig.largeContextThreshold}). Forced high tier.`,
254
- state.thinkingByProfile[model.id],
255
- false,
256
- );
257
- decision.isContextTriggered = true;
341
+ thinkingDecision = {
342
+ ...thinkingDecision,
343
+ tier: 'high',
344
+ phase: 'planning',
345
+ reasoning: `Context usage (${usage.tokens}) exceeds threshold (${state.currentConfig.largeContextThreshold}). Forced high thinking.`,
346
+ isContextTriggered: true,
347
+ };
258
348
  }
259
- } catch (e) {
349
+ } catch (_e) {
260
350
  // ignore
261
351
  }
262
352
  }
263
353
 
264
- // Classifier Override
265
354
  if (
266
355
  state.currentConfig.classifierModel &&
267
356
  !pinnedTier &&
268
- !decision.isContextTriggered &&
269
- !decision.isRuleMatched
357
+ !thinkingDecision.isContextTriggered &&
358
+ !thinkingDecision.isRuleMatched
270
359
  ) {
271
360
  const classifierResult = await runClassifier(
272
361
  state.currentConfig.classifierModel,
@@ -275,24 +364,34 @@ export const registerRouterProvider = (
275
364
  state.lastDecision?.phase,
276
365
  );
277
366
  if (classifierResult) {
278
- decision = buildRoutingDecision(
367
+ thinkingDecision = buildRoutingDecision(
279
368
  model.id,
280
369
  profile,
281
370
  classifierResult.tier,
282
371
  phaseForTier(classifierResult.tier),
283
- `Classifier: ${classifierResult.reasoning}`,
284
- state.thinkingByProfile[model.id],
372
+ `Thinking classifier: ${classifierResult.reasoning}`,
373
+ thinkingOverrides,
285
374
  true,
286
375
  );
287
- if (isBudgetExceeded && decision.tier === 'high') {
288
- decision.tier = 'medium';
289
- decision.phase = 'implementation';
290
- decision.reasoning = `Budget exceeded. Downgraded classifier decision to medium. (Original: ${decision.reasoning})`;
291
- decision.isBudgetForced = true;
376
+ if (isBudgetExceeded && thinkingDecision.tier === 'high') {
377
+ thinkingDecision = {
378
+ ...thinkingDecision,
379
+ tier: 'medium',
380
+ phase: 'implementation',
381
+ reasoning: `Budget exceeded. Downgraded thinking to medium. (Original: ${thinkingDecision.reasoning})`,
382
+ isBudgetForced: true,
383
+ };
292
384
  }
293
385
  }
294
386
  }
295
387
 
388
+ let decision = applyThinkingToDecision(
389
+ lockedBase,
390
+ thinkingDecision,
391
+ profile,
392
+ thinkingOverrides,
393
+ );
394
+
296
395
  const lastMessage = context.messages[context.messages.length - 1];
297
396
  const previousDecision = state.lastDecision;
298
397
  const isGoogleThinkingToolContinuation =
@@ -302,7 +401,7 @@ export const registerRouterProvider = (
302
401
  previousDecision.thinking !== 'off' &&
303
402
  decision.targetProvider === 'google' &&
304
403
  decision.thinking !== 'off' &&
305
- previousDecision.targetLabel !== decision.targetLabel;
404
+ previousDecision.targetLabel === decision.targetLabel;
306
405
 
307
406
  if (isGoogleThinkingToolContinuation) {
308
407
  decision = {
@@ -319,56 +418,6 @@ export const registerRouterProvider = (
319
418
  };
320
419
  }
321
420
 
322
- const imageAttached = hasImageAttachment(context);
323
- if (imageAttached) {
324
- const checkModelSupportsImage = (modelRef: string) => {
325
- try {
326
- const { provider, modelId } = parseCanonicalModelRef(modelRef);
327
- const m = state.currentModelRegistry?.find(provider, modelId);
328
- return m?.input?.includes('image') ?? false;
329
- } catch {
330
- return false;
331
- }
332
- };
333
-
334
- const tierModels = [
335
- decision.targetLabel,
336
- ...(profile[decision.tier].fallbacks ?? []),
337
- ];
338
- if (!tierModels.some(checkModelSupportsImage)) {
339
- const tiersToTry: RouterTier[] =
340
- decision.tier === 'low'
341
- ? ['medium', 'high']
342
- : decision.tier === 'medium'
343
- ? ['high']
344
- : [];
345
-
346
- let foundTier: RouterTier | undefined;
347
- for (const t of tiersToTry) {
348
- const tModels = [
349
- profile[t].model,
350
- ...(profile[t].fallbacks ?? []),
351
- ];
352
- if (tModels.some(checkModelSupportsImage)) {
353
- foundTier = t;
354
- break;
355
- }
356
- }
357
-
358
- if (foundTier) {
359
- decision = buildRoutingDecision(
360
- model.id,
361
- profile,
362
- foundTier,
363
- phaseForTier(foundTier),
364
- `Forced ${foundTier} tier because the originally routed ${decision.tier} tier does not support image attachments.`,
365
- state.thinkingByProfile[model.id],
366
- false,
367
- );
368
- }
369
- }
370
- }
371
-
372
421
  state.lastDecision = decision;
373
422
  actions.recordDebugDecision(decision);
374
423
 
@@ -376,10 +425,12 @@ export const registerRouterProvider = (
376
425
  actions.updateStatus(state.lastExtensionContext);
377
426
  }
378
427
 
428
+ const lockTier = lock.tier;
379
429
  let modelsToTry = [
380
- decision.targetLabel,
381
- ...(profile[decision.tier].fallbacks ?? []),
430
+ lock.modelRef,
431
+ ...(profile[lockTier].fallbacks ?? []),
382
432
  ];
433
+ const imageAttached = hasImageAttachment(context);
383
434
  if (imageAttached) {
384
435
  modelsToTry = modelsToTry.filter((modelRef) => {
385
436
  try {
@@ -7,6 +7,7 @@ import type {
7
7
  RoutingDecision,
8
8
  RoutingRule,
9
9
  RouterThinkingByTier,
10
+ SessionLock,
10
11
  } from './types.js';
11
12
  import { parseCanonicalModelRef, isRouterTier } from './config.js';
12
13
 
@@ -38,6 +39,153 @@ export const getLastUserText = (context: Context): string => {
38
39
  return '';
39
40
  };
40
41
 
42
+ export const getFirstUserText = (context: Context): string => {
43
+ for (const message of context.messages) {
44
+ if (message.role === 'user') {
45
+ return extractTextFromContent(message.content).trim();
46
+ }
47
+ }
48
+ return '';
49
+ };
50
+
51
+ /** Context for one-shot session model lock: system prompt + first user message only. */
52
+ export const buildSessionLockContext = (context: Context): Context => {
53
+ const firstUser = context.messages.find((message) => message.role === 'user');
54
+ const messages = firstUser ? [firstUser] : context.messages.slice(0, 1);
55
+ return { ...context, messages };
56
+ };
57
+
58
+ export const sessionLockToRoutingDecision = (
59
+ lock: SessionLock,
60
+ profile: RouterProfile,
61
+ thinkingOverrides?: RouterThinkingByTier,
62
+ ): RoutingDecision => {
63
+ const routed = profile[lock.tier];
64
+ const { provider, modelId } = parseCanonicalModelRef(lock.modelRef);
65
+ const baseThinking =
66
+ routed.thinking ??
67
+ (lock.tier === 'high' ? 'high' : lock.tier === 'low' ? 'low' : 'medium');
68
+ const effectiveThinking = thinkingOverrides?.[lock.tier] ?? baseThinking;
69
+ return {
70
+ profile: lock.profile,
71
+ tier: lock.tier,
72
+ phase: phaseForTier(lock.tier),
73
+ targetProvider: provider,
74
+ targetModelId: modelId,
75
+ targetLabel: lock.modelRef,
76
+ reasoning: lock.reasoning,
77
+ thinking: effectiveThinking,
78
+ timestamp: Date.now(),
79
+ };
80
+ };
81
+
82
+ export const routingDecisionToSessionLock = (
83
+ decision: RoutingDecision,
84
+ ): SessionLock => ({
85
+ profile: decision.profile,
86
+ tier: decision.tier,
87
+ modelRef: decision.targetLabel,
88
+ reasoning: decision.reasoning,
89
+ });
90
+
91
+ /** Text used to score initial session complexity (system prompt + first user message). */
92
+ export const getSessionLockAnalysisText = (context: Context): string => {
93
+ const lockContext = buildSessionLockContext(context);
94
+ const system = (context.systemPrompt ?? '').trim();
95
+ const firstUser = getFirstUserText(lockContext);
96
+ if (system && firstUser) return `${system}\n\n${firstUser}`;
97
+ return system || firstUser || '';
98
+ };
99
+
100
+ /**
101
+ * One-shot tier pick for session model lock (no prior-turn stickiness).
102
+ */
103
+ export const decideSessionLock = (
104
+ context: Context,
105
+ profileName: string,
106
+ profile: RouterProfile,
107
+ pinnedTier?: RouterTier,
108
+ thinkingOverrides?: RouterThinkingByTier,
109
+ phaseBias = 0.5,
110
+ rules?: RoutingRule[],
111
+ ): RoutingDecision => {
112
+ const analysisText = getSessionLockAnalysisText(context);
113
+ const synthetic: Context = {
114
+ systemPrompt: context.systemPrompt,
115
+ messages: analysisText
116
+ ? [{ role: 'user', content: analysisText, timestamp: Date.now() }]
117
+ : [],
118
+ };
119
+ return decideRouting(
120
+ synthetic,
121
+ profileName,
122
+ profile,
123
+ undefined,
124
+ pinnedTier,
125
+ thinkingOverrides,
126
+ phaseBias,
127
+ rules,
128
+ false,
129
+ );
130
+ };
131
+
132
+ /** Per-turn thinking tier merged onto a session-locked model decision. */
133
+ export const applyThinkingToDecision = (
134
+ locked: RoutingDecision,
135
+ thinkingDecision: RoutingDecision,
136
+ profile: RouterProfile,
137
+ thinkingOverrides?: RouterThinkingByTier,
138
+ ): RoutingDecision => {
139
+ const tier = thinkingDecision.tier;
140
+ const routed = profile[tier];
141
+ const baseThinking =
142
+ routed.thinking ??
143
+ (tier === 'high' ? 'high' : tier === 'low' ? 'low' : 'medium');
144
+ const effectiveThinking = thinkingOverrides?.[tier] ?? baseThinking;
145
+
146
+ return {
147
+ profile: locked.profile,
148
+ tier,
149
+ phase: thinkingDecision.phase,
150
+ targetProvider: locked.targetProvider,
151
+ targetModelId: locked.targetModelId,
152
+ targetLabel: locked.targetLabel,
153
+ reasoning: `${locked.reasoning} | Thinking: ${thinkingDecision.reasoning}`,
154
+ thinking: effectiveThinking,
155
+ timestamp: Date.now(),
156
+ isClassifier: thinkingDecision.isClassifier,
157
+ isRuleMatched: thinkingDecision.isRuleMatched,
158
+ isBudgetForced: thinkingDecision.isBudgetForced,
159
+ isContextTriggered: thinkingDecision.isContextTriggered,
160
+ };
161
+ };
162
+
163
+ /** Harness subagents: tier from system prompt + optional first user line. */
164
+ export const resolveTierFromPrompt = (
165
+ systemPrompt: string,
166
+ userText: string,
167
+ profileName: string,
168
+ profile: RouterProfile,
169
+ rules?: RoutingRule[],
170
+ phaseBias = 0.5,
171
+ ): RouterTier => {
172
+ const context: Context = {
173
+ systemPrompt,
174
+ messages: userText
175
+ ? [{ role: 'user', content: userText, timestamp: Date.now() }]
176
+ : [],
177
+ };
178
+ return decideSessionLock(
179
+ context,
180
+ profileName,
181
+ profile,
182
+ undefined,
183
+ undefined,
184
+ phaseBias,
185
+ rules,
186
+ ).tier;
187
+ };
188
+
41
189
  export const getRecentConversationText = (
42
190
  context: Context,
43
191
  limit = 6,
@@ -3,6 +3,7 @@ import type {
3
3
  RouterThinkingByProfile,
4
4
  RoutingDecision,
5
5
  RouterPersistedState,
6
+ SessionLock,
6
7
  } from './types.js';
7
8
 
8
9
  export const isRouterPersistedState = (
@@ -30,6 +31,7 @@ export const buildPersistedState = (
30
31
  lastDecision: RoutingDecision | undefined,
31
32
  lastNonRouterModel: string | undefined,
32
33
  accumulatedCost: number,
34
+ sessionLock: SessionLock | undefined,
33
35
  ): RouterPersistedState => {
34
36
  return {
35
37
  enabled: routerEnabled,
@@ -37,6 +39,7 @@ export const buildPersistedState = (
37
39
  pinTier: pinnedTierByProfile[selectedProfile],
38
40
  pinByProfile: { ...pinnedTierByProfile },
39
41
  thinkingByProfile: { ...thinkingByProfile },
42
+ sessionLock,
40
43
  debugEnabled,
41
44
  widgetEnabled,
42
45
  debugHistory,
@@ -53,12 +53,21 @@ export interface RoutingDecision {
53
53
  isRuleMatched?: boolean;
54
54
  }
55
55
 
56
+ /** Fixed model for a router profile for the lifetime of a session (or until profile switch). */
57
+ export interface SessionLock {
58
+ profile: string;
59
+ tier: RouterTier;
60
+ modelRef: string;
61
+ reasoning: string;
62
+ }
63
+
56
64
  export interface RouterPersistedState {
57
65
  enabled: boolean;
58
66
  selectedProfile: string;
59
67
  pinTier?: RouterTier;
60
68
  pinByProfile?: RouterPinByProfile;
61
69
  thinkingByProfile?: RouterThinkingByProfile;
70
+ sessionLock?: SessionLock;
62
71
  debugEnabled?: boolean;
63
72
  widgetEnabled?: boolean;
64
73
  debugHistory?: RoutingDecision[];
@@ -4,6 +4,7 @@ import type {
4
4
  RouterConfig,
5
5
  RouterPinByProfile,
6
6
  RouterThinkingByProfile,
7
+ SessionLock,
7
8
  } from './types.js';
8
9
 
9
10
  const getEffectiveThinking = (
@@ -63,6 +64,7 @@ export const updateStatus = (
63
64
  accumulatedCost: number,
64
65
  widgetEnabled: boolean,
65
66
  currentConfig: RouterConfig,
67
+ sessionLock: SessionLock | undefined,
66
68
  ) => {
67
69
  const activeRouterProfile = routerEnabled ? selectedProfile : undefined;
68
70
  const statusProfile = selectedProfile;
@@ -81,7 +83,13 @@ export const updateStatus = (
81
83
  activeRouterProfile,
82
84
  lastDecision,
83
85
  );
84
- statusText = `router:${activeRouterProfile}${pinLabel} -> ${lastDecision.tier} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${effectiveThinking})`;
86
+ const locked =
87
+ sessionLock?.profile === activeRouterProfile
88
+ ? sessionLock.modelRef
89
+ : lastDecision.targetLabel;
90
+ statusText = `router:${activeRouterProfile}${pinLabel} locked:${locked} thinking:${lastDecision.tier} (${effectiveThinking})`;
91
+ } else if (sessionLock?.profile === activeRouterProfile) {
92
+ statusText = `router:${activeRouterProfile}${pinLabel} locked:${sessionLock.modelRef} -> waiting`;
85
93
  } else {
86
94
  statusText = `router:${activeRouterProfile}${pinLabel} -> waiting`;
87
95
  }
@@ -104,6 +112,11 @@ export const updateStatus = (
104
112
  ? ` / $${currentConfig.maxSessionBudget.toFixed(2)}`
105
113
  : ''),
106
114
  ];
115
+ if (sessionLock && sessionLock.profile === statusProfile) {
116
+ widgetLines.push(
117
+ `Locked model: ${sessionLock.modelRef} (lock tier ${sessionLock.tier})`,
118
+ );
119
+ }
107
120
  if (lastDecision && lastDecision.profile === statusProfile) {
108
121
  const effectiveThinking = getEffectiveThinking(
109
122
  thinkingByProfile,
@@ -114,8 +127,9 @@ export const updateStatus = (
114
127
  const flagsStr = flags.length > 0 ? ` [${flags.join(',')}]` : '';
115
128
 
116
129
  widgetLines.push(
117
- `Route: ${lastDecision.tier}${flagsStr} -> ${lastDecision.targetProvider}/${lastDecision.targetModelId} (${effectiveThinking})`,
130
+ `Thinking: ${lastDecision.tier}${flagsStr} (${effectiveThinking})`,
118
131
  `Phase: ${lastDecision.phase}`,
132
+ `Delegate: ${lastDecision.targetProvider}/${lastDecision.targetModelId}`,
119
133
  );
120
134
  } else if (!routerEnabled && lastNonRouterModel) {
121
135
  widgetLines.push(`Fallback: ${lastNonRouterModel}`);
@@ -1,124 +0,0 @@
1
- ---
2
- description: Commit and push changes with AI-generated messages and pi-mono co-author
3
- argument-hint: "[optional-scope]"
4
- ---
5
-
6
- # git-sync — Commit, Push, Sync
7
-
8
- Commits all staged/unstaged changes, generates AI summary + conventional commit message, adds pi-mono co-author, and pushes to remote.
9
-
10
- ## Step 1 — Pre-flight checks
11
-
12
- ```bash
13
- git rev-parse --is-inside-work-tree
14
- git remote -v
15
- ```
16
-
17
- If no remote or not a git repo → block and inform user.
18
-
19
- ## Step 2 — Read co-author config
20
-
21
- Read `.pi/auto-commit.json` to get co-author details:
22
-
23
- ```bash
24
- cat .pi/auto-commit.json
25
- ```
26
-
27
- Extract:
28
- - `coAuthor.login` (default: "pi-mono")
29
- - `coAuthor.email` (default: "261679550+pi-mono@users.noreply.github.com")
30
- - `branch.protected` — list of protected branches
31
- - `branch.strategy` — branch naming strategy
32
- - `push.allowedRemotes` — allowed remote names
33
-
34
- ## Step 3 — Check branch protection
35
-
36
- ```bash
37
- CURRENT_BRANCH=$(git branch --show-current)
38
- ```
39
-
40
- If current branch is in `protected` list (main, master, release/*):
41
- - Create a feature branch using `branch.strategy` pattern:
42
- ```
43
- feat/<shortcode-reference>-<short-description>
44
- ```
45
- - Switch to the new branch before committing.
46
-
47
- ## Step 4 — Stage changes
48
-
49
- ```bash
50
- git add -A
51
- git diff --cached --stat
52
- ```
53
-
54
- ## Step 5 — Generate AI commit message
55
-
56
- Analyze the staged diff and generate:
57
-
58
- 1. **Summary** (1-2 sentences): What changed and why.
59
- 2. **Commit message** using conventional commits format from config:
60
-
61
- ```
62
- <type>(<scope>): <summary>
63
-
64
- <body with details>
65
- ```
66
-
67
- Rules:
68
- - `type`: feat, fix, docs, style, refactor, perf, test, chore, ci, build
69
- - `scope`: module or file-group affected (from `message.scopeDefault` if unclear)
70
- - `summary`: imperative mood, ≤72 chars
71
- - `body`: bullet list of key changes, keep concise
72
-
73
- **IMPORTANT**: The commit message MUST be generated by the AI based on the actual diff content. Do NOT use a generic placeholder. Read the diff, understand what changed, and write a precise message.
74
-
75
- ## Step 6 — Commit with co-author
76
-
77
- ```bash
78
- git commit -m "<type>(<scope>): <summary>" -m "<body>" -m "Co-authored-by: <login> <<email>>"
79
- ```
80
-
81
- Example with defaults:
82
-
83
- ```bash
84
- git commit -m "feat(harness): add git-sync skill" -m "- Created git-sync skill with AI commit messages
85
- - Added pi-mono co-author support
86
- - Protected branch detection" -m "Co-authored-by: pi-mono <261679550+pi-mono@users.noreply.github.com>"
87
- ```
88
-
89
- ## Step 7 — Push
90
-
91
- ```bash
92
- git push -u origin <branch>
93
- ```
94
-
95
- Use `-u` to set upstream for new branches. Only push to remotes in `push.allowedRemotes`.
96
-
97
- ## Step 8 — Report
98
-
99
- Output summary:
100
- ```
101
- ✓ Synced <branch> → origin/<branch>
102
- Commit: <short-hash> <summary>
103
- Co-author: pi-mono
104
- Files: <N> changed, <M> insertions(+), <D> deletions(-)
105
- ```
106
-
107
- ## Guard Rails
108
-
109
- - **No force push**: Never use `--force` or `--force-with-lease` unless user explicitly requests.
110
- - **Protected branches**: Never commit directly to protected branches; always create a feature branch.
111
- - **Conflict detection**: If `git push` fails due to conflict, do NOT auto-resolve. Report and ask user.
112
- - **Empty commit skip**: If no changes detected, skip commit and inform user.
113
- - **Dry run mode**: If `dryRun: true` in config, show what would be committed without actually committing.
114
- - **Submodules**: If `submodules.ignore: true`, do not stage submodule path changes.
115
-
116
- ## Error Handling
117
-
118
- | Error | Action |
119
- |-------|--------|
120
- | Not a git repo | Report, suggest `git init` |
121
- | No remote | Report, suggest `git remote add` |
122
- | Push rejected (conflict) | Report conflict, advise `git pull --rebase` |
123
- | Auth failure | Report, suggest checking credentials |
124
- | No changes | Report "nothing to commit", skip push |