@hobin/developer 0.1.7 → 0.1.9

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 (66) hide show
  1. package/README.md +212 -355
  2. package/REFERENCE_ROUTING.md +243 -0
  3. package/extensions/developer.ts +601 -23
  4. package/extensions/machine.ts +45 -0
  5. package/extensions/references/behavior-preserving-structural-change.md +87 -8
  6. package/extensions/skills.ts +330 -52
  7. package/extensions/state.ts +227 -2
  8. package/extensions/tool-policy.ts +87 -0
  9. package/package.json +9 -3
  10. package/skills/abstraction-review/SKILL.md +37 -23
  11. package/skills/abstraction-review/reference-policy.json +66 -0
  12. package/skills/abstraction-review/references/field-card.md +113 -168
  13. package/skills/abstraction-review/references/repair-table.md +66 -80
  14. package/skills/abstraction-review/references/worked-examples.md +79 -256
  15. package/skills/model/SKILL.md +31 -13
  16. package/skills/model/reference-policy.json +96 -0
  17. package/skills/model/references/contract-and-replacement-models.md +112 -0
  18. package/skills/model/references/logic-query-semantics.md +89 -0
  19. package/skills/model/references/planning-models.md +75 -0
  20. package/skills/model/references/problem-modeling.md +143 -276
  21. package/skills/model/references/proof-obligations.md +71 -0
  22. package/skills/model/references/relational-constraint-models.md +110 -0
  23. package/skills/model/references/solver-result-boundaries.md +83 -0
  24. package/skills/model/references/temporal-behavior-models.md +106 -0
  25. package/skills/naming-judgment/SKILL.md +17 -4
  26. package/skills/naming-judgment/reference-policy.json +29 -0
  27. package/skills/naming-judgment/references/domain-naming.md +29 -10
  28. package/skills/schedule/SKILL.md +16 -6
  29. package/skills/schedule/reference-policy.json +29 -0
  30. package/skills/schedule/references/structural-change-timing.md +81 -18
  31. package/skills/signal/SKILL.md +17 -6
  32. package/skills/signal/reference-policy.json +29 -0
  33. package/skills/signal/references/structural-movement.md +87 -14
  34. package/skills/sketch/SKILL.md +38 -42
  35. package/skills/sketch/reference-policy.json +212 -0
  36. package/skills/sketch/references/accumulator-invariants.md +125 -0
  37. package/skills/sketch/references/closure-and-conventional-interfaces.md +87 -0
  38. package/skills/sketch/references/composition-by-wishes.md +69 -0
  39. package/skills/sketch/references/data-driven-design.md +99 -22
  40. package/skills/sketch/references/data-shape-template-catalog.md +58 -19
  41. package/skills/sketch/references/design-levels-and-boundaries.md +133 -0
  42. package/skills/sketch/references/earned-abstraction.md +75 -0
  43. package/skills/sketch/references/generative-recursion.md +122 -0
  44. package/skills/sketch/references/generic-dispatch-systems.md +75 -0
  45. package/skills/sketch/references/language-semantics.md +82 -0
  46. package/skills/sketch/references/meaning-preserving-conversions.md +69 -0
  47. package/skills/sketch/references/process-shape-and-resources.md +157 -0
  48. package/skills/sketch/references/representation-barriers.md +73 -0
  49. package/skills/sketch/references/responsibility-and-collaboration.md +108 -0
  50. package/skills/sketch/references/runtime-and-compilation.md +84 -0
  51. package/skills/sketch/references/selection-and-creation.md +76 -0
  52. package/skills/sketch/references/state-history-and-order.md +126 -0
  53. package/skills/sketch/references/type-transitions.md +54 -0
  54. package/skills/sketch/references/variation-roles.md +59 -0
  55. package/skills/verify/SKILL.md +17 -6
  56. package/skills/verify/reference-policy.json +30 -0
  57. package/skills/verify/references/verifier-selection-and-pass-but-wrong.md +152 -192
  58. package/SOURCES.md +0 -104
  59. package/skills/abstraction-review/references/recipe-cards.md +0 -322
  60. package/skills/model/references/worked-models-and-specialized-techniques.md +0 -218
  61. package/skills/sketch/references/abstraction-barriers-and-closure.md +0 -160
  62. package/skills/sketch/references/abstraction-composition-and-state.md +0 -184
  63. package/skills/sketch/references/composition-generative-recursion-and-accumulators.md +0 -196
  64. package/skills/sketch/references/generic-operations-and-languages.md +0 -134
  65. package/skills/sketch/references/processes-state-and-time.md +0 -171
  66. package/skills/sketch/references/responsibility-and-variation.md +0 -245
@@ -18,7 +18,14 @@ import {
18
18
  import { Container, Markdown, Text } from "@earendil-works/pi-tui";
19
19
  import { Type, type Static } from "typebox";
20
20
 
21
- import { availablePackageSkills, renderSkillMethod } from "./skills.ts";
21
+ import {
22
+ availablePackageSkills,
23
+ isWithinRoot,
24
+ loadSkillReference,
25
+ loadSkillReferencePolicy,
26
+ renderSkillMethod,
27
+ skillReferencePaths,
28
+ } from "./skills.ts";
22
29
  import {
23
30
  ACTIVATION_ENTRY,
24
31
  FOCUS_ENTRY,
@@ -28,6 +35,7 @@ import {
28
35
  MAX_RESPONSE_OPTIONS,
29
36
  MAX_RESPONSE_TEXT_CHARS,
30
37
  PROTOCOL,
38
+ REFERENCE_TOOL,
31
39
  ROUTE_TOOL,
32
40
  applyDeveloperEvent,
33
41
  canApplyDeveloperEvent,
@@ -49,13 +57,21 @@ import {
49
57
  type QuestionResolutionOwner,
50
58
  type QuestionUpdate,
51
59
  type QuestionUpdateStatus,
60
+ type ReferenceBasis,
61
+ type ReferenceExemption,
62
+ type ReferenceLoadEvent,
63
+ type ReferencePolicyExemption,
64
+ type ReferencePolicyRoute,
52
65
  type RouteAlternative,
53
66
  type RouteEvent,
54
67
  } from "./state.ts";
55
68
  import {
69
+ TOOL_POLICY_LIFECYCLE_ENTRY,
56
70
  builtinControlledToolCapabilities,
57
71
  isControlledToolAllowed,
58
72
  reconcileProtocolTools,
73
+ reloadSafeToolPolicyMarker,
74
+ toolPolicyReloadRequiresRestart,
59
75
  type ProtocolToolAccess,
60
76
  type ToolPolicyMemory,
61
77
  } from "./tool-policy.ts";
@@ -73,7 +89,7 @@ import {
73
89
  type ImmediateQuestionDisposition,
74
90
  } from "./tui.ts";
75
91
 
76
- const PROTOCOL_TOOLS = [ROUTE_TOOL, JUDGMENT_TOOL] as const;
92
+ const PROTOCOL_TOOLS = [ROUTE_TOOL, REFERENCE_TOOL, JUDGMENT_TOOL] as const;
77
93
  const extensionRoot = dirname(fileURLToPath(import.meta.url));
78
94
  const skillsRoot = resolve(extensionRoot, "..", "skills");
79
95
  const structuralChangeMethodPath = resolve(
@@ -88,6 +104,8 @@ const MAX_EVIDENCE_CHARS = 2_000;
88
104
  const MAX_RESULT_CHARS = 12_000;
89
105
  const MAX_ARTIFACT_CHARS = 4_096;
90
106
  const DEVELOPER_COMMAND_ACTIONS = ["on", "status", "questions", "off"] as const;
107
+ const TOOL_POLICY_RESTART_MESSAGE =
108
+ "Developer detected an in-process package reload from a version without reload-safe tool handoff. Restart the Pi process before enabling Developer; /reload and /develop off/on cannot safely reconstruct the prior built-in tool selection.";
91
109
 
92
110
  function textResult<T>(text: string, details: T) {
93
111
  return {
@@ -137,6 +155,27 @@ function compactLine(value: string, maxChars = 160): string {
137
155
  return line.length <= maxChars ? line : `${line.slice(0, maxChars - 1)}…`;
138
156
  }
139
157
 
158
+ function referenceRouteSummary(route: ReferencePolicyRoute): string {
159
+ return `${route.id}: question=${route.question} | trigger=${route.trigger} | refines=${route.methodStep} | references=${route.references.join(" + ")} (read order: ${route.readOrder}) | produces=${route.artifacts.join("; ")} | stop=${route.stop} | separate when=${route.separateWhen}`;
160
+ }
161
+
162
+ function referencePolicyLines(
163
+ routes: ReferencePolicyRoute[],
164
+ exemption: ReferencePolicyExemption | undefined,
165
+ ): string[] {
166
+ if (routes.length === 0) return [];
167
+ return [
168
+ "Reference routes (each route extends one named judgment step and answers one narrower question; its references are one co-required set, while read order is declared separately):",
169
+ ...routes.map((route) => `- ${referenceRouteSummary(route)}`),
170
+ ...(exemption
171
+ ? [
172
+ `Reference exemption applies only when: ${exemption.when}`,
173
+ `Exemption evidence: ${exemption.evidence.join(" | ")}`,
174
+ ]
175
+ : []),
176
+ ];
177
+ }
178
+
140
179
  function normalizedQuestion(value: string): string {
141
180
  return value
142
181
  .toLocaleLowerCase()
@@ -408,11 +447,13 @@ function protocolPrompt(
408
447
  "Developer protocol is active.",
409
448
  "- Use the default topology as a conditional backbone, not a rigid lifecycle: clarify meaning when needed -> model consequential cases -> sketch the first implementation surface for new behavior or signal the smallest structural movement in existing code -> execute one implementation step -> verify current claims.",
410
449
  "- Adapt away from that topology when evidence makes a stage not applicable, but never jump directly from a resolved model to mutation: use sketch for feature implementation or signal for existing-code structural movement first.",
450
+ "- Route by the requested work product, not keywords: use sketch to create an original caller-facing interface, representation boundary, ownership map, or collaboration; use abstraction-review only when a concrete candidate surface already exists to keep/revise/split/reject/defer; use model first only when unresolved cases, rules, or policy can materially change that surface.",
411
451
  `- Call ${ROUTE_TOOL} for exactly one concrete judgment or one green-to-green implementation movement.`,
412
452
  "- Use target=implementation only when the next local movement, stable landing, and narrow verification are already justified; otherwise choose the focused skill whose scope fits the current question.",
413
453
  "- An implementation step has one observable difference and one structural or behavioral purpose. Stop when its failure is locally explainable and the repository is green, pausable, and reviewable.",
414
454
  "- For implementation structural work intended to preserve behavior, set execution_profile=behavior-preserving-structure; omit the field for other implementation actions and every skill route.",
415
455
  `- Follow the routed method, then close the route with ${JUDGMENT_TOOL}. After every implementation stable landing, route again from the new evidence before selecting another movement.`,
456
+ `- During a reference-bearing skill route, select every policy route whose observable trigger addresses a distinct unresolved question. Each selected route must refine its declared skill-method step, produce its declared artifact, satisfy its stop condition, and hand off instead of absorbing work covered by separate_when. Load every reference in each route's co-required set with ${REFERENCE_TOOL} in its explicit order. Do not select a broad fallback route when a narrower selected route already includes its work. A resolved judgment must apply every loaded reference in reference_basis, or use reference_exemption only when no route trigger applies.`,
416
457
  "- Consecutive implementation routes are valid when the new evidence still justifies implementation action. On a reroute after implementation, explicitly reconsider the most plausible skill routes and record why they are not needed; do not choose implementation by momentum.",
417
458
  "- Do not carry a predetermined implementation queue through multiple implementation steps. Re-observe after each stable landing and reroute to a skill whenever meaning, cases, design, structural direction, timing, naming, or evidence becomes uncertain.",
418
459
  "- Protocol state is routing bookkeeping. Idle never proves product completion, user acceptance, or current verification.",
@@ -460,6 +501,9 @@ function protocolPrompt(
460
501
  if (state.activeRoute.methodLocation) {
461
502
  lines.push(
462
503
  `Active skill location: ${state.activeRoute.methodLocation}. Read it again if compaction or a later turn no longer contains the full instructions.`,
504
+ `Available routed references: ${state.activeRoute.availableReferences.join(", ") || "none"}.`,
505
+ `Reference routes: ${state.activeRoute.referenceRoutes.map((route) => `${route.id}[${route.references.join("+")}; order=${route.readOrder}]`).join(" | ") || "none"}. Re-read the skill's reference-policy.json if trigger or artifact details were compacted.`,
506
+ `Loaded routed references: ${state.activeRoute.loadedReferences.map((reference) => `${reference.path}@${reference.contentSha256.slice(0, 12)}[${reference.referenceRouteIds.join(",") || "legacy"}]`).join(", ") || "none"}.`,
463
507
  );
464
508
  }
465
509
  }
@@ -529,6 +573,26 @@ function expandedJudgment(
529
573
 
530
574
  const evidence = [
531
575
  ...event.basis.map((basis) => detailLine(theme, "basis", basis)),
576
+ ...(event.referenceBasis ?? []).flatMap((reference) => [
577
+ detailLine(
578
+ theme,
579
+ "reference",
580
+ `${reference.path}@${reference.contentSha256.slice(0, 12)} [${reference.referenceRouteIds.join(", ") || "legacy"}] — trigger: ${reference.trigger}`,
581
+ "accent",
582
+ ),
583
+ detailLine(theme, "applied rule", reference.appliedRule, "text"),
584
+ detailLine(theme, "resulting artifact", reference.artifact, "text"),
585
+ ]),
586
+ ...(event.referenceExemption
587
+ ? [
588
+ detailLine(
589
+ theme,
590
+ "reference exemption",
591
+ `${event.referenceExemption.reason} — ${event.referenceExemption.evidence.join(" | ")}`,
592
+ "warning",
593
+ ),
594
+ ]
595
+ : []),
532
596
  ...event.artifacts.map((artifact) =>
533
597
  detailLine(theme, "artifact", artifact),
534
598
  ),
@@ -560,6 +624,7 @@ export default async function developer(pi: ExtensionAPI) {
560
624
  let routeOpening = false;
561
625
  const routesWithMutation = new Set<string>();
562
626
  let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
627
+ let toolPolicyRestartRequired = false;
563
628
 
564
629
  pi.registerFlag("develop", {
565
630
  description: "Start with the Developer protocol enabled",
@@ -582,7 +647,31 @@ export default async function developer(pi: ExtensionAPI) {
582
647
  pi.setActiveTools(next.activeTools);
583
648
  };
584
649
 
650
+ const releaseProtocolTools = () => {
651
+ const current = pi.getActiveTools();
652
+ const next = reconcileProtocolTools({
653
+ activeTools: current,
654
+ allTools: pi.getAllTools(),
655
+ enabled: false,
656
+ access: protocolToolAccess(state),
657
+ protocolTools: PROTOCOL_TOOLS,
658
+ memory: toolPolicyMemory,
659
+ });
660
+ toolPolicyMemory = next.memory;
661
+ if (!sameToolSet(current, next.activeTools))
662
+ pi.setActiveTools(next.activeTools);
663
+ };
664
+
585
665
  const refreshUI = (ctx: ExtensionContext) => {
666
+ if (toolPolicyRestartRequired) {
667
+ ctx.ui.setStatus("developer", "developer · restart required");
668
+ ctx.ui.setWidget(
669
+ "developer",
670
+ ["blocked · restart Pi to reset Developer tool access"],
671
+ { placement: "belowEditor" },
672
+ );
673
+ return;
674
+ }
586
675
  if (!state.enabled) {
587
676
  ctx.ui.setStatus("developer", undefined);
588
677
  ctx.ui.setWidget("developer", undefined);
@@ -643,12 +732,19 @@ export default async function developer(pi: ExtensionAPI) {
643
732
  };
644
733
 
645
734
  const reconstruct = (ctx: ExtensionContext) => {
646
- state = reconstructState(ctx.sessionManager.getBranch());
735
+ state = toolPolicyRestartRequired
736
+ ? initialState()
737
+ : reconstructState(ctx.sessionManager.getBranch());
647
738
  syncProtocolTools();
648
739
  refreshUI(ctx);
649
740
  };
650
741
 
651
- const setEnabled = (enabled: boolean, ctx: ExtensionContext) => {
742
+ const setEnabled = (enabled: boolean, ctx: ExtensionContext): boolean => {
743
+ if (enabled && toolPolicyRestartRequired) {
744
+ ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
745
+ refreshUI(ctx);
746
+ return false;
747
+ }
652
748
  const event: ActivationEvent = {
653
749
  protocol: PROTOCOL,
654
750
  kind: "activation",
@@ -658,6 +754,7 @@ export default async function developer(pi: ExtensionAPI) {
658
754
  state = applyDeveloperEvent(state, event);
659
755
  syncProtocolTools();
660
756
  refreshUI(ctx);
757
+ return true;
661
758
  };
662
759
 
663
760
  const SharedRouteParams = {
@@ -768,11 +865,11 @@ export default async function developer(pi: ExtensionAPI) {
768
865
  name: ROUTE_TOOL,
769
866
  label: "Developer Route Question",
770
867
  description:
771
- "Route one concrete judgment or one green-to-green implementation movement. Uses an adaptive default topology: model, then sketch for feature shape or signal for structural movement, implementation stable landings, and verify before completion.",
868
+ "Route one concrete judgment or one green-to-green implementation movement. Route by work product: sketch creates an original design surface, abstraction-review judges an already-shaped candidate, and model settles unresolved conditions before design. Then use implementation stable landings and verify before completion.",
772
869
  promptSnippet: "Choose how to handle one development question",
773
870
  promptGuidelines: [
774
871
  `Call ${ROUTE_TOOL} only when there is no active Developer route.`,
775
- `Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; target=implementation requires one movement, one stable landing, and one narrow verification.`,
872
+ `Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; choose sketch for an original interface or boundary, abstraction-review only for an already-shaped candidate, and model only for unresolved condition space. target=implementation requires one movement, one stable landing, and one narrow verification.`,
776
873
  `When ${ROUTE_TOOL} follows an implementation judgment with another implementation route, cite the previous landing in known_evidence and record plausible skill routes in alternatives_considered instead of selecting implementation by momentum.`,
777
874
  `After a resolved model, use sketch for first feature implementation framing or signal for existing-code structural movement before implementation mutation.`,
778
875
  ],
@@ -896,20 +993,29 @@ export default async function developer(pi: ExtensionAPI) {
896
993
  ? (requestedExecutionProfile ?? "ordinary")
897
994
  : undefined;
898
995
 
899
- const method =
900
- target !== "implementation"
901
- ? await renderSkillMethod(skill!)
902
- : executionProfile === "behavior-preserving-structure"
903
- ? [
904
- '<developer-implementation-profile name="behavior-preserving-structure">',
905
- (await readFile(structuralChangeMethodPath, "utf8")).trim(),
906
- "</developer-implementation-profile>",
907
- ].join("\n")
908
- : [
909
- "# Implementation action",
910
- "",
911
- "The next local action is already justified. Keep this route open while using Pi implementation tools and collecting evidence.",
912
- ].join("\n");
996
+ const availableReferences = skill
997
+ ? await skillReferencePaths(skill)
998
+ : [];
999
+ const referencePolicy = skill
1000
+ ? await loadSkillReferencePolicy(skill, availableReferences)
1001
+ : { routes: [] };
1002
+ let method: string;
1003
+ if (skill) {
1004
+ method = await renderSkillMethod(skill);
1005
+ } else if (executionProfile === "behavior-preserving-structure") {
1006
+ const profile = await readFile(structuralChangeMethodPath, "utf8");
1007
+ method = [
1008
+ '<developer-implementation-profile name="behavior-preserving-structure">',
1009
+ profile.trim(),
1010
+ "</developer-implementation-profile>",
1011
+ ].join("\n");
1012
+ } else {
1013
+ method = [
1014
+ "# Implementation action",
1015
+ "",
1016
+ "The next local action is already justified. Keep this route open while using Pi implementation tools and collecting evidence.",
1017
+ ].join("\n");
1018
+ }
913
1019
 
914
1020
  const implementationStep =
915
1021
  target === "implementation"
@@ -938,6 +1044,11 @@ export default async function developer(pi: ExtensionAPI) {
938
1044
  reason,
939
1045
  knownEvidence,
940
1046
  consideredAlternatives,
1047
+ availableReferences,
1048
+ referenceRoutes: referencePolicy.routes,
1049
+ referenceExemptionCriteria: referencePolicy.exemption,
1050
+ referencePolicySha256: referencePolicy.contentSha256,
1051
+ loadedReferences: [],
941
1052
  targetQuestionId,
942
1053
  methodLocation: skill?.filePath,
943
1054
  executionProfile,
@@ -962,6 +1073,17 @@ export default async function developer(pi: ExtensionAPI) {
962
1073
  ]
963
1074
  : []),
964
1075
  `Reason: ${event.reason}`,
1076
+ `Available references: ${event.availableReferences.join(" | ") || "none"}`,
1077
+ ...referencePolicyLines(
1078
+ event.referenceRoutes,
1079
+ event.referenceExemptionCriteria,
1080
+ ),
1081
+ `Reference policy SHA-256: ${event.referencePolicySha256 ?? "none"}`,
1082
+ ...(skill && event.availableReferences.length > 0
1083
+ ? [
1084
+ `Reference contract: choose policy routes by observable trigger, use ${REFERENCE_TOOL} for every reference in each selected route's co-required set, and provide reference_basis for every loaded path; use reference_exemption only when no policy route applies.`,
1085
+ ]
1086
+ : []),
965
1087
  `Known evidence: ${event.knownEvidence.length > 0 ? event.knownEvidence.join(" | ") : "none"}`,
966
1088
  `Alternatives reconsidered: ${
967
1089
  event.consideredAlternatives.length > 0
@@ -1042,6 +1164,15 @@ export default async function developer(pi: ExtensionAPI) {
1042
1164
  }
1043
1165
  text += `\n${detailLine(theme, "revisits", event.targetQuestionId ?? "none")}`;
1044
1166
  text += `\n${detailLine(theme, "skill", event.methodLocation ?? "implementation action")}`;
1167
+ if ((event.availableReferences ?? []).length > 0) {
1168
+ text += `\n${detailLine(theme, "references", event.availableReferences.join(", "))}`;
1169
+ }
1170
+ for (const route of event.referenceRoutes ?? []) {
1171
+ text += `\n${detailLine(theme, `reference route ${route.id}`, `${route.question} · ${route.trigger} → ${route.references.join(" + ")} · step=${route.methodStep} · stop=${route.stop} · separate=${route.separateWhen}`)}`;
1172
+ }
1173
+ if (event.referencePolicySha256) {
1174
+ text += `\n${detailLine(theme, "reference policy", event.referencePolicySha256)}`;
1175
+ }
1045
1176
  if (event.executionProfile) {
1046
1177
  text += `\n${detailLine(theme, "profile", event.executionProfile)}`;
1047
1178
  }
@@ -1057,6 +1188,192 @@ export default async function developer(pi: ExtensionAPI) {
1057
1188
  },
1058
1189
  });
1059
1190
 
1191
+ const ReferenceParams = Type.Object(
1192
+ {
1193
+ reference_route: Type.String({
1194
+ minLength: 1,
1195
+ maxLength: 128,
1196
+ description:
1197
+ "Exact reference route ID listed by the active Developer skill policy",
1198
+ }),
1199
+ path: Type.String({
1200
+ minLength: 1,
1201
+ maxLength: MAX_ARTIFACT_CHARS,
1202
+ description:
1203
+ "Exact direct skill-relative reference path listed by the active route, such as references/data-driven-design.md",
1204
+ }),
1205
+ reason: Type.String({
1206
+ minLength: 1,
1207
+ maxLength: MAX_EVIDENCE_CHARS,
1208
+ description:
1209
+ "Observable task evidence that makes this reference relevant now",
1210
+ }),
1211
+ },
1212
+ { additionalProperties: false },
1213
+ );
1214
+
1215
+ pi.registerTool({
1216
+ name: REFERENCE_TOOL,
1217
+ label: "Developer Load Reference",
1218
+ description:
1219
+ "Select one active policy route while loading one reference from its co-required set, recording route ID, exact path, content SHA-256, Source Trace, and trigger evidence.",
1220
+ promptSnippet: "Load an active Developer skill reference with provenance",
1221
+ promptGuidelines: [
1222
+ `Use ${REFERENCE_TOOL}, not an ordinary read, for any reference that supports an active Developer skill judgment.`,
1223
+ `Call ${REFERENCE_TOOL} with a policy reference_route and one not-yet-loaded path from that route's co-required set; state the observable trigger and apply it only at the declared method step. One successful load satisfies an overlapping path for every selected route, so never reload a shared path and provide only one reference_basis entry per distinct path.`,
1224
+ ],
1225
+ parameters: ReferenceParams,
1226
+ executionMode: "sequential",
1227
+ renderShell: "self",
1228
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1229
+ if (!state.enabled) fail("Developer protocol is off.");
1230
+ const activeRoute = state.activeRoute;
1231
+ if (!activeRoute) fail("There is no active Developer skill route.");
1232
+ if (activeRoute.target === "implementation") {
1233
+ fail("Implementation routes do not load skill references.");
1234
+ }
1235
+ const referenceRouteId = params.reference_route.trim();
1236
+ const path = params.path.trim().replaceAll("\\", "/");
1237
+ const reason = params.reason.trim();
1238
+ if (!referenceRouteId || !path || !reason) {
1239
+ fail(
1240
+ "Reference route, path, and reason must contain non-whitespace text.",
1241
+ );
1242
+ }
1243
+ const skill = availableSkills.get(activeRoute.target);
1244
+ if (!skill || skill.filePath !== activeRoute.methodLocation) {
1245
+ fail(
1246
+ "The active skill source is no longer available at the route's canonical location. Route again against the current Pi resources.",
1247
+ );
1248
+ }
1249
+ const availableReferences =
1250
+ activeRoute.availableReferences.length > 0
1251
+ ? activeRoute.availableReferences
1252
+ : await skillReferencePaths(skill);
1253
+ let referenceRoutes = activeRoute.referenceRoutes;
1254
+ if (referenceRoutes.length === 0) {
1255
+ const policy = await loadSkillReferencePolicy(
1256
+ skill,
1257
+ availableReferences,
1258
+ );
1259
+ referenceRoutes = policy.routes;
1260
+ }
1261
+ const policyRoute = referenceRoutes.find(
1262
+ (candidate) => candidate.id === referenceRouteId,
1263
+ );
1264
+ if (!policyRoute) {
1265
+ fail(
1266
+ `Unknown reference route ${referenceRouteId}. Available routes: ${referenceRoutes.map((route) => route.id).join(", ") || "none"}.`,
1267
+ );
1268
+ }
1269
+ if (!policyRoute.references.includes(path)) {
1270
+ fail(
1271
+ `Reference ${path} is not part of route ${policyRoute.id}. Co-required route references: ${policyRoute.references.join(", ")}.`,
1272
+ );
1273
+ }
1274
+ const loadedPaths = new Set(
1275
+ activeRoute.loadedReferences.map((reference) => reference.path),
1276
+ );
1277
+ if (loadedPaths.has(path)) {
1278
+ fail(
1279
+ `Reference ${path} is already loaded. One successful load satisfies overlapping route membership; select ${policyRoute.id} by loading one of its not-yet-loaded paths and keep one reference_basis entry per distinct path.`,
1280
+ );
1281
+ }
1282
+ if (policyRoute.readOrder === "listed") {
1283
+ const pathIndex = policyRoute.references.indexOf(path);
1284
+ const missingEarlierReferences = policyRoute.references
1285
+ .slice(0, pathIndex)
1286
+ .filter((reference) => !loadedPaths.has(reference));
1287
+ if (missingEarlierReferences.length > 0) {
1288
+ fail(
1289
+ `Reference route ${policyRoute.id} declares listed read order. Load these earlier route references first: ${missingEarlierReferences.join(", ")}.`,
1290
+ );
1291
+ }
1292
+ }
1293
+ const reference = await loadSkillReference(skill, path);
1294
+ const event: ReferenceLoadEvent = {
1295
+ protocol: PROTOCOL,
1296
+ kind: "reference-load",
1297
+ routeId: activeRoute.routeId,
1298
+ target: activeRoute.target,
1299
+ path: reference.path,
1300
+ reason,
1301
+ contentSha256: reference.contentSha256,
1302
+ sourceTrace: reference.sourceTrace,
1303
+ referenceRouteIds: [policyRoute.id],
1304
+ };
1305
+ if (!canApplyDeveloperEvent(state, event)) {
1306
+ fail(
1307
+ "Developer machine guard rejected the reference load for the current active route.",
1308
+ );
1309
+ }
1310
+ const response = [
1311
+ `Route ID: ${event.routeId}`,
1312
+ `Target: ${event.target}`,
1313
+ `Reference policy route: ${policyRoute.id}`,
1314
+ `Route question: ${policyRoute.question}`,
1315
+ `Route trigger: ${policyRoute.trigger}`,
1316
+ `Skill method step refined: ${policyRoute.methodStep}`,
1317
+ `Co-required references: ${policyRoute.references.join(" + ")}`,
1318
+ `Read order: ${policyRoute.readOrder}`,
1319
+ `Expected application artifacts: ${policyRoute.artifacts.join(" | ")}`,
1320
+ `Route stop: ${policyRoute.stop}`,
1321
+ `Separate or hand off when: ${policyRoute.separateWhen}`,
1322
+ `Reference: ${event.path}`,
1323
+ `Reference SHA-256: ${event.contentSha256}`,
1324
+ `Source Trace: ${event.sourceTrace ? "present in loaded content and recorded in tool details" : "not declared by this reference"}`,
1325
+ `Trigger evidence: ${event.reason}`,
1326
+ "",
1327
+ "<developer-reference>",
1328
+ reference.content,
1329
+ "</developer-reference>",
1330
+ ].join("\n");
1331
+ ensureSafeToolText(response, "Developer reference result");
1332
+ state = applyDeveloperEvent(state, event);
1333
+ syncProtocolTools();
1334
+ refreshUI(ctx);
1335
+ return textResult(response, event);
1336
+ },
1337
+ renderCall(args, theme, context) {
1338
+ const path =
1339
+ typeof args.path === "string" && args.path.length > 0 ? args.path : "…";
1340
+ return reusableText(
1341
+ `${theme.fg("toolTitle", theme.bold(REFERENCE_TOOL))} ${theme.fg("accent", path)}`,
1342
+ context.lastComponent,
1343
+ );
1344
+ },
1345
+ renderResult(result, { expanded, isPartial }, theme, context) {
1346
+ if (isPartial) {
1347
+ return reusableText(
1348
+ theme.fg("warning", "loading Developer reference…"),
1349
+ context.lastComponent,
1350
+ );
1351
+ }
1352
+ if (context.isError) {
1353
+ return reusableText(
1354
+ theme.fg("error", resultText(result) || "Developer reference failed"),
1355
+ context.lastComponent,
1356
+ );
1357
+ }
1358
+ const event = result.details as ReferenceLoadEvent | undefined;
1359
+ let text = theme.fg(
1360
+ "success",
1361
+ `loaded ${event?.path ?? "Developer reference"}`,
1362
+ );
1363
+ if (expanded && event) {
1364
+ text += `\n${detailLine(theme, "route", event.routeId, "text")}`;
1365
+ text += `\n${detailLine(theme, "target", event.target, "accent")}`;
1366
+ text += `\n${detailLine(theme, "reference route", event.referenceRouteIds.join(", ") || "legacy", "accent")}`;
1367
+ text += `\n${detailLine(theme, "sha256", event.contentSha256)}`;
1368
+ text += `\n${detailLine(theme, "trigger", event.reason, "text")}`;
1369
+ text += `\n${detailLine(theme, "source trace", event.sourceTrace || "not declared", event.sourceTrace ? "muted" : "warning")}`;
1370
+ }
1371
+ if (!expanded && event)
1372
+ text += ` · ${keyHint("app.tools.expand", "provenance")}`;
1373
+ return reusableText(text, context.lastComponent);
1374
+ },
1375
+ });
1376
+
1060
1377
  const ChoiceResponseOptionParam = Type.Object(
1061
1378
  {
1062
1379
  value: Type.String({
@@ -1166,6 +1483,54 @@ export default async function developer(pi: ExtensionAPI) {
1166
1483
  },
1167
1484
  { additionalProperties: false },
1168
1485
  );
1486
+ const ReferenceBasisParam = Type.Object(
1487
+ {
1488
+ path: Type.String({
1489
+ minLength: 1,
1490
+ maxLength: MAX_ARTIFACT_CHARS,
1491
+ description: `A reference path successfully loaded through ${REFERENCE_TOOL} during this route`,
1492
+ }),
1493
+ trigger: Type.String({
1494
+ minLength: 1,
1495
+ maxLength: MAX_EVIDENCE_CHARS,
1496
+ description:
1497
+ "Observable input, code, state, or requirement condition that made the reference applicable",
1498
+ }),
1499
+ applied_rule: Type.String({
1500
+ minLength: 1,
1501
+ maxLength: MAX_EVIDENCE_CHARS,
1502
+ description:
1503
+ "The concrete derivation, diagnostic, or decision rule taken from the loaded reference",
1504
+ }),
1505
+ artifact: Type.String({
1506
+ minLength: 1,
1507
+ maxLength: MAX_ARTIFACT_CHARS,
1508
+ description:
1509
+ "The result table, model, interface, trace, decision, or check produced by applying that rule",
1510
+ }),
1511
+ },
1512
+ { additionalProperties: false },
1513
+ );
1514
+ const ReferenceExemptionParam = Type.Object(
1515
+ {
1516
+ reason: Type.String({
1517
+ minLength: 1,
1518
+ maxLength: MAX_EVIDENCE_CHARS,
1519
+ description:
1520
+ "Why none of the active skill's available references can add useful judgment for this resolved local question",
1521
+ }),
1522
+ evidence: Type.Array(
1523
+ Type.String({ minLength: 1, maxLength: MAX_EVIDENCE_CHARS }),
1524
+ {
1525
+ minItems: 1,
1526
+ maxItems: 8,
1527
+ description:
1528
+ "Concrete facts supporting the exemption rather than a convenience claim",
1529
+ },
1530
+ ),
1531
+ },
1532
+ { additionalProperties: false },
1533
+ );
1169
1534
  const JudgmentParams = Type.Object({
1170
1535
  route_id: Type.String({
1171
1536
  minLength: 1,
@@ -1188,6 +1553,14 @@ export default async function developer(pi: ExtensionAPI) {
1188
1553
  maxItems: 20,
1189
1554
  description: "Evidence supporting the judgment or blocker",
1190
1555
  }),
1556
+ reference_basis: Type.Optional(
1557
+ Type.Array(ReferenceBasisParam, {
1558
+ maxItems: 12,
1559
+ description:
1560
+ "Loaded references linked to their trigger, applied rule, and resulting judgment artifact",
1561
+ }),
1562
+ ),
1563
+ reference_exemption: Type.Optional(ReferenceExemptionParam),
1191
1564
  open_questions: Type.Optional(
1192
1565
  Type.Array(OpenQuestionParam, {
1193
1566
  maxItems: 10,
@@ -1219,6 +1592,7 @@ export default async function developer(pi: ExtensionAPI) {
1219
1592
  promptGuidelines: [
1220
1593
  `Use ${JUDGMENT_TOOL} with the exact active Developer route ID.`,
1221
1594
  `Use ${JUDGMENT_TOOL} result as Markdown and preserve the routed skill's inspectable tables, diagrams, matrices, timelines, and code blocks instead of reducing them to prose.`,
1595
+ `For a resolved skill route with available references, use ${JUDGMENT_TOOL} reference_basis to connect each relied-on ${REFERENCE_TOOL} load to its trigger, applied rule, and resulting artifact, or use reference_exemption when no reference is relevant. Never provide both.`,
1222
1596
  `Use ${JUDGMENT_TOOL} open_questions with resolution_owner, gate, and resolution_criteria. User-owned gated questions require explanatory context before controls; for finite required decisions, add a choice-form response_spec with one field per decision. Use question_updates whenever current evidence settles or changes any existing pending question.`,
1223
1597
  `Do not use ${JUDGMENT_TOOL} with resolved, not-applicable, or blocked status without at least one concrete basis.`,
1224
1598
  ],
@@ -1253,6 +1627,34 @@ export default async function developer(pi: ExtensionAPI) {
1253
1627
  if (params.route_id !== activeRoute.routeId) {
1254
1628
  fail(`Route ID mismatch. Active route is ${activeRoute.routeId}.`);
1255
1629
  }
1630
+ const activeSkill =
1631
+ activeRoute.target === "implementation"
1632
+ ? undefined
1633
+ : availableSkills.get(activeRoute.target);
1634
+ let availableReferences: string[] = [];
1635
+ if (activeRoute.target !== "implementation") {
1636
+ if (activeRoute.availableReferences.length > 0) {
1637
+ availableReferences = activeRoute.availableReferences;
1638
+ } else if (
1639
+ activeSkill &&
1640
+ activeSkill.filePath === activeRoute.methodLocation
1641
+ ) {
1642
+ availableReferences = await skillReferencePaths(activeSkill);
1643
+ }
1644
+ }
1645
+ let referenceRoutes = activeRoute.referenceRoutes;
1646
+ if (
1647
+ activeRoute.target !== "implementation" &&
1648
+ referenceRoutes.length === 0 &&
1649
+ activeSkill &&
1650
+ activeSkill.filePath === activeRoute.methodLocation
1651
+ ) {
1652
+ const policy = await loadSkillReferencePolicy(
1653
+ activeSkill,
1654
+ availableReferences,
1655
+ );
1656
+ referenceRoutes = policy.routes;
1657
+ }
1256
1658
 
1257
1659
  const basis = params.basis.map((item) => item.trim()).filter(Boolean);
1258
1660
  const result = params.result.trim();
@@ -1261,6 +1663,131 @@ export default async function developer(pi: ExtensionAPI) {
1261
1663
  fail(`${params.status} judgments require at least one concrete basis.`);
1262
1664
  }
1263
1665
 
1666
+ const referenceBasisInput = (params.reference_basis ?? []).map(
1667
+ (item) => ({
1668
+ path: item.path.trim().replaceAll("\\", "/"),
1669
+ trigger: item.trigger.trim(),
1670
+ appliedRule: item.applied_rule.trim(),
1671
+ artifact: item.artifact.trim(),
1672
+ }),
1673
+ );
1674
+ const referenceExemptionInput = params.reference_exemption
1675
+ ? {
1676
+ reason: params.reference_exemption.reason.trim(),
1677
+ evidence: params.reference_exemption.evidence
1678
+ .map((item) => item.trim())
1679
+ .filter(Boolean),
1680
+ }
1681
+ : undefined;
1682
+ if (referenceBasisInput.length > 0 && referenceExemptionInput) {
1683
+ fail("Use reference_basis or reference_exemption, never both.");
1684
+ }
1685
+ if (
1686
+ new Set(referenceBasisInput.map((item) => item.path)).size !==
1687
+ referenceBasisInput.length
1688
+ ) {
1689
+ fail("Each reference_basis path must be unique within a judgment.");
1690
+ }
1691
+ if (
1692
+ activeRoute.target === "implementation" &&
1693
+ (referenceBasisInput.length > 0 || referenceExemptionInput)
1694
+ ) {
1695
+ fail(
1696
+ "Implementation judgments do not accept skill reference evidence.",
1697
+ );
1698
+ }
1699
+ if (referenceExemptionInput && params.status !== "resolved") {
1700
+ fail(
1701
+ "reference_exemption is valid only for a resolved skill judgment.",
1702
+ );
1703
+ }
1704
+ if (referenceExemptionInput && availableReferences.length === 0) {
1705
+ fail(
1706
+ "reference_exemption is unnecessary because the active skill has no available references.",
1707
+ );
1708
+ }
1709
+ if (referenceExemptionInput && activeRoute.loadedReferences.length > 0) {
1710
+ fail(
1711
+ "reference_exemption is valid only when no routed reference was loaded during this route.",
1712
+ );
1713
+ }
1714
+ if (
1715
+ params.status === "resolved" &&
1716
+ availableReferences.length > 0 &&
1717
+ referenceBasisInput.length === 0 &&
1718
+ !referenceExemptionInput
1719
+ ) {
1720
+ fail(
1721
+ `A resolved ${activeRoute.target} judgment must provide reference_basis for references loaded through ${REFERENCE_TOOL}, or a concrete reference_exemption.`,
1722
+ );
1723
+ }
1724
+ const referenceBasisPaths = new Set(
1725
+ referenceBasisInput.map((item) => item.path),
1726
+ );
1727
+ const loadedReferences = new Map(
1728
+ activeRoute.loadedReferences.map((reference) => [
1729
+ reference.path,
1730
+ reference,
1731
+ ]),
1732
+ );
1733
+ const loadedPaths = new Set(loadedReferences.keys());
1734
+ const basisMissingForLoaded = [...loadedPaths].filter(
1735
+ (path) => !referenceBasisPaths.has(path),
1736
+ );
1737
+ if (basisMissingForLoaded.length > 0) {
1738
+ fail(
1739
+ `Every loaded reference must have a reference_basis entry: ${basisMissingForLoaded.join(", ")}.`,
1740
+ );
1741
+ }
1742
+ const selectedReferenceRouteIds = new Set(
1743
+ activeRoute.loadedReferences.flatMap(
1744
+ (reference) => reference.referenceRouteIds,
1745
+ ),
1746
+ );
1747
+ for (const referenceRouteId of selectedReferenceRouteIds) {
1748
+ const route = referenceRoutes.find(
1749
+ (candidate) => candidate.id === referenceRouteId,
1750
+ );
1751
+ if (!route) {
1752
+ fail(
1753
+ `Loaded reference selected unknown policy route ${referenceRouteId}; route again against the current skill policy.`,
1754
+ );
1755
+ }
1756
+ const missingRouteReferences = route.references.filter(
1757
+ (path) => !loadedPaths.has(path),
1758
+ );
1759
+ if (missingRouteReferences.length > 0) {
1760
+ fail(
1761
+ `Reference route ${route.id} is incomplete. Load its co-required references before judgment: ${missingRouteReferences.join(", ")}.`,
1762
+ );
1763
+ }
1764
+ }
1765
+ for (const loaded of activeRoute.loadedReferences) {
1766
+ for (const referenceRouteId of loaded.referenceRouteIds) {
1767
+ const route = referenceRoutes.find(
1768
+ (candidate) => candidate.id === referenceRouteId,
1769
+ );
1770
+ if (route && !route.references.includes(loaded.path)) {
1771
+ fail(
1772
+ `Reference ${loaded.path} is not part of its recorded policy route ${route.id}.`,
1773
+ );
1774
+ }
1775
+ }
1776
+ }
1777
+ const referenceBasis: ReferenceBasis[] = referenceBasisInput.map(
1778
+ (item) => {
1779
+ const loaded = loadedReferences.get(item.path);
1780
+ if (!loaded) {
1781
+ fail(
1782
+ `reference_basis path ${item.path} was not successfully loaded through ${REFERENCE_TOOL} during this route.`,
1783
+ );
1784
+ }
1785
+ return { ...loaded, ...item };
1786
+ },
1787
+ );
1788
+ const referenceExemption: ReferenceExemption | undefined =
1789
+ referenceExemptionInput;
1790
+
1264
1791
  const openedQuestions = buildOpenedQuestions(
1265
1792
  params.open_questions ?? [],
1266
1793
  state,
@@ -1361,6 +1888,8 @@ export default async function developer(pi: ExtensionAPI) {
1361
1888
  status: params.status,
1362
1889
  result,
1363
1890
  basis,
1891
+ referenceBasis,
1892
+ referenceExemption,
1364
1893
  openedQuestions,
1365
1894
  questionUpdates,
1366
1895
  artifacts: (params.artifacts ?? [])
@@ -1576,9 +2105,10 @@ export default async function developer(pi: ExtensionAPI) {
1576
2105
  return true;
1577
2106
  };
1578
2107
 
1579
- const setAndNotifyEnabled = (enabled: boolean) => {
1580
- setEnabled(enabled, ctx);
2108
+ const setAndNotifyEnabled = (enabled: boolean): boolean => {
2109
+ if (!setEnabled(enabled, ctx)) return false;
1581
2110
  ctx.ui.notify(`Developer: ${enabled ? "on" : "off"}`, "info");
2111
+ return true;
1582
2112
  };
1583
2113
 
1584
2114
  const turnOff = async (): Promise<boolean> => {
@@ -1603,6 +2133,11 @@ export default async function developer(pi: ExtensionAPI) {
1603
2133
  };
1604
2134
 
1605
2135
  const inspectStatus = async () => {
2136
+ if (toolPolicyRestartRequired) {
2137
+ ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
2138
+ refreshUI(ctx);
2139
+ return;
2140
+ }
1606
2141
  refreshAvailableSkills(ctx);
1607
2142
  if (ctx.mode === "tui") {
1608
2143
  await showDeveloperStatus(ctx, {
@@ -1752,6 +2287,30 @@ export default async function developer(pi: ExtensionAPI) {
1752
2287
  });
1753
2288
 
1754
2289
  pi.on("tool_call", (event) => {
2290
+ if (
2291
+ state.enabled &&
2292
+ event.toolName === "read" &&
2293
+ state.activeRoute?.target !== "implementation"
2294
+ ) {
2295
+ const activeSkill = state.activeRoute
2296
+ ? availableSkills.get(state.activeRoute.target)
2297
+ : undefined;
2298
+ const requestedPath = event.input.path;
2299
+ if (
2300
+ activeSkill &&
2301
+ typeof requestedPath === "string" &&
2302
+ isWithinRoot(
2303
+ resolve(activeSkill.baseDir, "references"),
2304
+ resolve(requestedPath),
2305
+ )
2306
+ ) {
2307
+ return {
2308
+ block: true,
2309
+ reason: `Developer skill references must be loaded through ${REFERENCE_TOOL} with a policy route so provenance and application remain auditable.`,
2310
+ };
2311
+ }
2312
+ }
2313
+
1755
2314
  const capability = builtinControlledToolCapabilities(pi.getAllTools()).get(
1756
2315
  event.toolName,
1757
2316
  );
@@ -1796,14 +2355,33 @@ export default async function developer(pi: ExtensionAPI) {
1796
2355
  };
1797
2356
  });
1798
2357
 
1799
- pi.on("session_start", (_event, ctx) => {
2358
+ pi.on("session_start", (event, ctx) => {
2359
+ const branch = ctx.sessionManager.getBranch();
2360
+ toolPolicyRestartRequired =
2361
+ event.reason === "reload" &&
2362
+ toolPolicyReloadRequiresRestart({
2363
+ entries: branch,
2364
+ protocol: PROTOCOL,
2365
+ protocolTools: PROTOCOL_TOOLS,
2366
+ });
1800
2367
  reconstruct(ctx);
2368
+ if (toolPolicyRestartRequired) {
2369
+ ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
2370
+ return;
2371
+ }
1801
2372
  const startEnabled = pi.getFlag("develop");
1802
2373
  if (startEnabled === true && !state.enabled) setEnabled(true, ctx);
1803
2374
  });
1804
2375
  pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
1805
2376
  pi.on("agent_settled", (_event, ctx) => refreshUI(ctx));
1806
2377
  pi.on("session_shutdown", (_event, ctx) => {
2378
+ releaseProtocolTools();
2379
+ if (!toolPolicyRestartRequired) {
2380
+ pi.appendEntry(
2381
+ TOOL_POLICY_LIFECYCLE_ENTRY,
2382
+ reloadSafeToolPolicyMarker(PROTOCOL),
2383
+ );
2384
+ }
1807
2385
  ctx.ui.setStatus("developer", undefined);
1808
2386
  ctx.ui.setWidget("developer", undefined);
1809
2387
  });