@hobin/developer 0.1.7 → 0.1.8

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.
package/README.md CHANGED
@@ -74,6 +74,14 @@ workspace boundary. Developer recognizes Pi built-ins from their provenance,
74
74
  preserves unrelated active tools, and does not force-enable tools the user
75
75
  disabled.
76
76
 
77
+ Before Pi tears down a session runtime, Developer releases its route-bound tool
78
+ delta so a reload, session replacement, or later extension instance does not
79
+ inherit orphaned restrictions. After that release, the package records a
80
+ reload-safe lifecycle marker. If an in-process reload finds older Developer
81
+ history without a later release marker, Developer stays off and requires a full
82
+ Pi process restart. Neither `/reload` nor `/develop off` → `on` can safely infer
83
+ which built-ins the user originally enabled.
84
+
77
85
  ## How a request runs
78
86
 
79
87
  Developer adds two model-facing protocol tools:
@@ -300,6 +308,10 @@ eligibility are derived from the same machine snapshot.
300
308
  The current event contract is `developer/v5`. Earlier protocol entries are not
301
309
  replayed; this breaking contract uses `target`, `implementation`, and
302
310
  `before-implementation` consistently across tools, events, and machine state.
311
+ A process started on the current package may open a branch containing those
312
+ entries, but hot-reloading directly from a pre-handoff runtime is rejected with
313
+ a restart instruction so its in-memory tool ownership cannot be mistaken for
314
+ user configuration.
303
315
 
304
316
  Developer deliberately leaves compaction ownership to Pi. It does not trigger,
305
317
  cancel, or replace threshold/overflow compaction, so it cannot override the
@@ -402,9 +414,11 @@ not append entries, submit answers, register tools, mutate active tools, restore
402
414
  sessions, start network work, or send model messages. Prepared answers are shown
403
415
  only as a character count.
404
416
 
405
- `check` validates package structure and deterministic behavior. `eval` launches
406
- the real Pi RPC surface without a model and covers package resources, commands,
407
- activation state, and route-bound tool gating. These are release-gating checks.
417
+ `check` validates package structure and deterministic behavior, including
418
+ reload migration detection and shutdown restoration of Developer-owned tool
419
+ deltas. `eval` launches the real Pi RPC surface without a model and covers
420
+ package resources, commands, activation state, and route-bound tool gating.
421
+ These are release-gating checks.
408
422
 
409
423
  Model-dependent runs are probabilistic evaluations rather than binary tests:
410
424
 
@@ -53,9 +53,12 @@ import {
53
53
  type RouteEvent,
54
54
  } from "./state.ts";
55
55
  import {
56
+ TOOL_POLICY_LIFECYCLE_ENTRY,
56
57
  builtinControlledToolCapabilities,
57
58
  isControlledToolAllowed,
58
59
  reconcileProtocolTools,
60
+ reloadSafeToolPolicyMarker,
61
+ toolPolicyReloadRequiresRestart,
59
62
  type ProtocolToolAccess,
60
63
  type ToolPolicyMemory,
61
64
  } from "./tool-policy.ts";
@@ -88,6 +91,8 @@ const MAX_EVIDENCE_CHARS = 2_000;
88
91
  const MAX_RESULT_CHARS = 12_000;
89
92
  const MAX_ARTIFACT_CHARS = 4_096;
90
93
  const DEVELOPER_COMMAND_ACTIONS = ["on", "status", "questions", "off"] as const;
94
+ const TOOL_POLICY_RESTART_MESSAGE =
95
+ "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
96
 
92
97
  function textResult<T>(text: string, details: T) {
93
98
  return {
@@ -560,6 +565,7 @@ export default async function developer(pi: ExtensionAPI) {
560
565
  let routeOpening = false;
561
566
  const routesWithMutation = new Set<string>();
562
567
  let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
568
+ let toolPolicyRestartRequired = false;
563
569
 
564
570
  pi.registerFlag("develop", {
565
571
  description: "Start with the Developer protocol enabled",
@@ -582,7 +588,31 @@ export default async function developer(pi: ExtensionAPI) {
582
588
  pi.setActiveTools(next.activeTools);
583
589
  };
584
590
 
591
+ const releaseProtocolTools = () => {
592
+ const current = pi.getActiveTools();
593
+ const next = reconcileProtocolTools({
594
+ activeTools: current,
595
+ allTools: pi.getAllTools(),
596
+ enabled: false,
597
+ access: protocolToolAccess(state),
598
+ protocolTools: PROTOCOL_TOOLS,
599
+ memory: toolPolicyMemory,
600
+ });
601
+ toolPolicyMemory = next.memory;
602
+ if (!sameToolSet(current, next.activeTools))
603
+ pi.setActiveTools(next.activeTools);
604
+ };
605
+
585
606
  const refreshUI = (ctx: ExtensionContext) => {
607
+ if (toolPolicyRestartRequired) {
608
+ ctx.ui.setStatus("developer", "developer · restart required");
609
+ ctx.ui.setWidget(
610
+ "developer",
611
+ ["blocked · restart Pi to reset Developer tool access"],
612
+ { placement: "belowEditor" },
613
+ );
614
+ return;
615
+ }
586
616
  if (!state.enabled) {
587
617
  ctx.ui.setStatus("developer", undefined);
588
618
  ctx.ui.setWidget("developer", undefined);
@@ -643,12 +673,19 @@ export default async function developer(pi: ExtensionAPI) {
643
673
  };
644
674
 
645
675
  const reconstruct = (ctx: ExtensionContext) => {
646
- state = reconstructState(ctx.sessionManager.getBranch());
676
+ state = toolPolicyRestartRequired
677
+ ? initialState()
678
+ : reconstructState(ctx.sessionManager.getBranch());
647
679
  syncProtocolTools();
648
680
  refreshUI(ctx);
649
681
  };
650
682
 
651
- const setEnabled = (enabled: boolean, ctx: ExtensionContext) => {
683
+ const setEnabled = (enabled: boolean, ctx: ExtensionContext): boolean => {
684
+ if (enabled && toolPolicyRestartRequired) {
685
+ ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
686
+ refreshUI(ctx);
687
+ return false;
688
+ }
652
689
  const event: ActivationEvent = {
653
690
  protocol: PROTOCOL,
654
691
  kind: "activation",
@@ -658,6 +695,7 @@ export default async function developer(pi: ExtensionAPI) {
658
695
  state = applyDeveloperEvent(state, event);
659
696
  syncProtocolTools();
660
697
  refreshUI(ctx);
698
+ return true;
661
699
  };
662
700
 
663
701
  const SharedRouteParams = {
@@ -1576,9 +1614,10 @@ export default async function developer(pi: ExtensionAPI) {
1576
1614
  return true;
1577
1615
  };
1578
1616
 
1579
- const setAndNotifyEnabled = (enabled: boolean) => {
1580
- setEnabled(enabled, ctx);
1617
+ const setAndNotifyEnabled = (enabled: boolean): boolean => {
1618
+ if (!setEnabled(enabled, ctx)) return false;
1581
1619
  ctx.ui.notify(`Developer: ${enabled ? "on" : "off"}`, "info");
1620
+ return true;
1582
1621
  };
1583
1622
 
1584
1623
  const turnOff = async (): Promise<boolean> => {
@@ -1603,6 +1642,11 @@ export default async function developer(pi: ExtensionAPI) {
1603
1642
  };
1604
1643
 
1605
1644
  const inspectStatus = async () => {
1645
+ if (toolPolicyRestartRequired) {
1646
+ ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
1647
+ refreshUI(ctx);
1648
+ return;
1649
+ }
1606
1650
  refreshAvailableSkills(ctx);
1607
1651
  if (ctx.mode === "tui") {
1608
1652
  await showDeveloperStatus(ctx, {
@@ -1796,14 +1840,33 @@ export default async function developer(pi: ExtensionAPI) {
1796
1840
  };
1797
1841
  });
1798
1842
 
1799
- pi.on("session_start", (_event, ctx) => {
1843
+ pi.on("session_start", (event, ctx) => {
1844
+ const branch = ctx.sessionManager.getBranch();
1845
+ toolPolicyRestartRequired =
1846
+ event.reason === "reload" &&
1847
+ toolPolicyReloadRequiresRestart({
1848
+ entries: branch,
1849
+ protocol: PROTOCOL,
1850
+ protocolTools: PROTOCOL_TOOLS,
1851
+ });
1800
1852
  reconstruct(ctx);
1853
+ if (toolPolicyRestartRequired) {
1854
+ ctx.ui.notify(TOOL_POLICY_RESTART_MESSAGE, "error");
1855
+ return;
1856
+ }
1801
1857
  const startEnabled = pi.getFlag("develop");
1802
1858
  if (startEnabled === true && !state.enabled) setEnabled(true, ctx);
1803
1859
  });
1804
1860
  pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
1805
1861
  pi.on("agent_settled", (_event, ctx) => refreshUI(ctx));
1806
1862
  pi.on("session_shutdown", (_event, ctx) => {
1863
+ releaseProtocolTools();
1864
+ if (!toolPolicyRestartRequired) {
1865
+ pi.appendEntry(
1866
+ TOOL_POLICY_LIFECYCLE_ENTRY,
1867
+ reloadSafeToolPolicyMarker(PROTOCOL),
1868
+ );
1869
+ }
1807
1870
  ctx.ui.setStatus("developer", undefined);
1808
1871
  ctx.ui.setWidget("developer", undefined);
1809
1872
  });
@@ -1,5 +1,8 @@
1
1
  export type ControlledToolCapability = "shell" | "artifact";
2
2
 
3
+ export const TOOL_POLICY_LIFECYCLE_ENTRY = "developer.tool-policy-lifecycle";
4
+ export const TOOL_POLICY_LIFECYCLE = "released-before-reload/v1";
5
+
3
6
  const CONTROLLED_BUILTIN_CAPABILITIES = new Map<
4
7
  string,
5
8
  ControlledToolCapability
@@ -29,6 +32,90 @@ export interface ProtocolToolAccess {
29
32
  hasBeforeImplementationGate: boolean;
30
33
  }
31
34
 
35
+ interface ToolPolicyBranchEntry {
36
+ type?: string;
37
+ customType?: string;
38
+ data?: unknown;
39
+ message?: {
40
+ role?: string;
41
+ toolName?: string;
42
+ details?: unknown;
43
+ };
44
+ }
45
+
46
+ interface ToolPolicyLifecycleMarker {
47
+ protocol: string;
48
+ kind: "tool-policy-lifecycle";
49
+ lifecycle: typeof TOOL_POLICY_LIFECYCLE;
50
+ }
51
+
52
+ function isObject(value: unknown): value is Record<string, unknown> {
53
+ return typeof value === "object" && value !== null;
54
+ }
55
+
56
+ export function reloadSafeToolPolicyMarker(
57
+ protocol: string,
58
+ ): ToolPolicyLifecycleMarker {
59
+ return {
60
+ protocol,
61
+ kind: "tool-policy-lifecycle",
62
+ lifecycle: TOOL_POLICY_LIFECYCLE,
63
+ };
64
+ }
65
+
66
+ function isReloadSafeToolPolicyMarker(
67
+ entry: ToolPolicyBranchEntry,
68
+ protocol: string,
69
+ ): boolean {
70
+ if (
71
+ entry.type !== "custom" ||
72
+ entry.customType !== TOOL_POLICY_LIFECYCLE_ENTRY ||
73
+ !isObject(entry.data)
74
+ )
75
+ return false;
76
+ return (
77
+ entry.data.protocol === protocol &&
78
+ entry.data.kind === "tool-policy-lifecycle" &&
79
+ entry.data.lifecycle === TOOL_POLICY_LIFECYCLE
80
+ );
81
+ }
82
+
83
+ export function toolPolicyReloadRequiresRestart(input: {
84
+ entries: readonly ToolPolicyBranchEntry[];
85
+ protocol: string;
86
+ protocolTools: readonly string[];
87
+ }): boolean {
88
+ const protocolTools = new Set(input.protocolTools);
89
+ for (let index = input.entries.length - 1; index >= 0; index -= 1) {
90
+ const entry = input.entries[index];
91
+ if (!entry) continue;
92
+ if (isReloadSafeToolPolicyMarker(entry, input.protocol)) return false;
93
+
94
+ let value: unknown;
95
+ if (
96
+ entry.type === "custom" &&
97
+ entry.customType?.startsWith("developer.") &&
98
+ entry.customType !== TOOL_POLICY_LIFECYCLE_ENTRY
99
+ ) {
100
+ value = entry.data;
101
+ } else if (
102
+ entry.type === "message" &&
103
+ entry.message?.role === "toolResult" &&
104
+ entry.message.toolName &&
105
+ protocolTools.has(entry.message.toolName)
106
+ ) {
107
+ value = entry.message.details;
108
+ }
109
+ if (
110
+ isObject(value) &&
111
+ typeof value.protocol === "string" &&
112
+ value.protocol.startsWith("developer/")
113
+ )
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+
32
119
  export function builtinControlledToolCapabilities(
33
120
  tools: ToolMetadataLike[],
34
121
  ): Map<string, ControlledToolCapability> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobin/developer",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Adaptive product-development reasoning and evidence for Pi.",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "pi", "developer", "skills"],