@jwilger/pi-development-system 0.1.0 → 0.1.2
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.
|
@@ -10,22 +10,28 @@ const nonNegotiables = readFileSync(
|
|
|
10
10
|
);
|
|
11
11
|
|
|
12
12
|
/** Composition root: wires modules, holds no logic. */
|
|
13
|
-
export
|
|
13
|
+
export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
14
14
|
const state = createSessionState(pi);
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
let lastCtx: ExtensionContext | undefined;
|
|
17
|
+
|
|
18
|
+
const refreshStatus = () => {
|
|
19
|
+
lastCtx?.ui.setStatus(STATUS_KEY, renderStatusLine(state.get()));
|
|
18
20
|
};
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const rebuild = (ctx: ExtensionContext) => {
|
|
23
|
+
lastCtx = ctx;
|
|
24
|
+
state.rebuildFrom(
|
|
25
|
+
ctx.sessionManager
|
|
26
|
+
.getBranch()
|
|
27
|
+
.flatMap((e) => (e.type === "custom" ? [{ customType: e.customType, data: e.data }] : [])),
|
|
28
|
+
);
|
|
29
|
+
refreshStatus();
|
|
30
|
+
};
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
32
|
+
state.onChange(refreshStatus);
|
|
33
|
+
pi.on("session_start", (_event, ctx) => rebuild(ctx));
|
|
34
|
+
pi.on("session_tree", (_event, ctx) => rebuild(ctx));
|
|
29
35
|
|
|
30
36
|
pi.on("before_agent_start", (event) => {
|
|
31
37
|
applyPromptSection(event, state.get(), nonNegotiables);
|
|
@@ -37,12 +43,10 @@ export default function developmentSystem(pi: ExtensionAPI): void {
|
|
|
37
43
|
ctx.ui.notify(renderStatus(state.get()), "info");
|
|
38
44
|
},
|
|
39
45
|
});
|
|
46
|
+
|
|
47
|
+
return { state };
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
const e = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
45
|
-
return e.type === "custom" && typeof e.customType === "string"
|
|
46
|
-
? [{ customType: e.customType, data: e.data }]
|
|
47
|
-
: [];
|
|
50
|
+
export default function developmentSystem(pi: ExtensionAPI): void {
|
|
51
|
+
createDevelopmentSystem(pi);
|
|
48
52
|
}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ Amend/rebase/reset of pushed refs, `--force*`, deleting shared branches. Because
|
|
|
10
10
|
## 2. Never weaken verification to make a gate pass
|
|
11
11
|
Deleting, skipping or loosening tests; suppressing a lint; editing expected values to match output; `--no-verify`. Because a green gate must mean the same thing afterwards. (Changing a test because the *requirement* changed is a recorded departure, not this.)
|
|
12
12
|
|
|
13
|
-
## 3. Never claim something was verified, run or green
|
|
13
|
+
## 3. Never claim something was verified, run, or green that was not
|
|
14
14
|
Because the record of evidence is the product. Evidence comes before claims.
|
|
15
15
|
|
|
16
16
|
## 4. Never push on a red trunk
|
|
@@ -22,8 +22,8 @@ Stop, record, ask. Because a gate you can route around is not a gate.
|
|
|
22
22
|
## 6. Never violate repo-local delivery policy
|
|
23
23
|
Delivery mode, protected branches, required reviews. Because the repo, not this tool, owns policy.
|
|
24
24
|
|
|
25
|
-
## 7. Never let secrets leave the machine
|
|
26
|
-
|
|
25
|
+
## 7. Never let secrets leave the machine without sanitisation
|
|
26
|
+
In commits, logs, eval cases, or subagent prompts. Because exfiltration is irreversible.
|
|
27
27
|
|
|
28
28
|
## 8. Never commit without a rationale-bearing Conventional Commit, or add AI attribution trailers
|
|
29
29
|
No `Co-Authored-By`, no `Generated-by`. Because the message is the durable why; attribution noise is not.
|
package/src/core/types.ts
CHANGED
|
@@ -5,9 +5,7 @@ export type ParseError = { readonly kind: "parse-error"; readonly message: strin
|
|
|
5
5
|
export const parseError = (message: string): ParseError => ({ kind: "parse-error", message });
|
|
6
6
|
|
|
7
7
|
export const isParseError = (value: unknown): value is ParseError =>
|
|
8
|
-
typeof value === "object" &&
|
|
9
|
-
value !== null &&
|
|
10
|
-
(value as { kind?: unknown }).kind === "parse-error";
|
|
8
|
+
typeof value === "object" && value !== null && "kind" in value && value.kind === "parse-error";
|
|
11
9
|
|
|
12
10
|
export type Tier = "hard" | "soft" | "advisory";
|
|
13
11
|
|
|
@@ -28,13 +28,37 @@ const JEV: ReadonlyArray<JevStatus> = ["online", "offline", "unknown"];
|
|
|
28
28
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
29
29
|
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
30
30
|
|
|
31
|
+
const isString = (v: unknown): v is string => typeof v === "string";
|
|
32
|
+
|
|
33
|
+
/** Structural check for a persisted Departure; I1.1's parseDeparture should supersede it. */
|
|
34
|
+
function isDeparture(v: unknown): v is Departure {
|
|
35
|
+
if (!isRecord(v)) return false;
|
|
36
|
+
const scope = v["scope"];
|
|
37
|
+
const scopeOk =
|
|
38
|
+
isRecord(scope) &&
|
|
39
|
+
(scope["kind"] === "session" ||
|
|
40
|
+
(scope["kind"] === "slice" && isString(scope["slice"])) ||
|
|
41
|
+
(scope["kind"] === "once" && isString(scope["toolCallId"])));
|
|
42
|
+
return (
|
|
43
|
+
scopeOk &&
|
|
44
|
+
["id", "gate", "default", "chosen", "why", "costIfWrong", "recordedAt"].every((k) =>
|
|
45
|
+
isString(v[k]),
|
|
46
|
+
) &&
|
|
47
|
+
(v["tier"] === "soft" || v["tier"] === "hard") &&
|
|
48
|
+
(v["approver"] === "agent" || v["approver"] === "user") &&
|
|
49
|
+
(v["revisitWhen"] === undefined || isString(v["revisitWhen"]))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
/** Boundary parse for persisted state: the only place state is cast from `unknown`. */
|
|
32
54
|
export function parseDevsysState(input: unknown): DevsysState | ParseError {
|
|
33
55
|
if (!isRecord(input)) return parseError("devsys state must be an object");
|
|
34
56
|
const { phase, sizing, activeSlice, openDepartures, jev, lastPushAt } = input;
|
|
35
57
|
if (!PHASES.includes(phase as Phase)) return parseError(`unknown phase: ${String(phase)}`);
|
|
36
58
|
if (!JEV.includes(jev as JevStatus)) return parseError(`unknown jev status: ${String(jev)}`);
|
|
37
|
-
if (!Array.isArray(openDepartures)
|
|
59
|
+
if (!Array.isArray(openDepartures) || !openDepartures.every(isDeparture)) {
|
|
60
|
+
return parseError("openDepartures must be an array of valid departures");
|
|
61
|
+
}
|
|
38
62
|
if (sizing !== undefined && !SIZINGS.includes(sizing as Sizing)) {
|
|
39
63
|
return parseError(`unknown sizing: ${String(sizing)}`);
|
|
40
64
|
}
|
|
@@ -47,7 +71,7 @@ export function parseDevsysState(input: unknown): DevsysState | ParseError {
|
|
|
47
71
|
return {
|
|
48
72
|
phase: phase as Phase,
|
|
49
73
|
jev: jev as JevStatus,
|
|
50
|
-
openDepartures: openDepartures
|
|
74
|
+
openDepartures: openDepartures,
|
|
51
75
|
...(sizing !== undefined ? { sizing: sizing as Sizing } : {}),
|
|
52
76
|
...(activeSlice !== undefined ? { activeSlice: activeSlice as SliceRef } : {}),
|
|
53
77
|
...(lastPushAt !== undefined ? { lastPushAt } : {}),
|
|
@@ -58,6 +82,8 @@ export type SessionState = {
|
|
|
58
82
|
get(): DevsysState;
|
|
59
83
|
update(fn: (s: DevsysState) => DevsysState): void;
|
|
60
84
|
rebuildFrom(entries: ReadonlyArray<{ customType: string; data: unknown }>): void;
|
|
85
|
+
/** Registers a listener run after every update(); returns an unsubscribe function. */
|
|
86
|
+
onChange(listener: (state: DevsysState) => void): () => void;
|
|
61
87
|
};
|
|
62
88
|
|
|
63
89
|
/**
|
|
@@ -66,11 +92,19 @@ export type SessionState = {
|
|
|
66
92
|
*/
|
|
67
93
|
export function createSessionState(api: ExtensionAPI): SessionState {
|
|
68
94
|
let current: DevsysState = initialState();
|
|
95
|
+
const listeners = new Set<(state: DevsysState) => void>();
|
|
69
96
|
return {
|
|
70
97
|
get: () => current,
|
|
71
98
|
update(fn) {
|
|
72
99
|
current = fn(current);
|
|
73
100
|
api.appendEntry(STATE_ENTRY_TYPE, current);
|
|
101
|
+
for (const listener of listeners) listener(current);
|
|
102
|
+
},
|
|
103
|
+
onChange(listener) {
|
|
104
|
+
listeners.add(listener);
|
|
105
|
+
return () => {
|
|
106
|
+
listeners.delete(listener);
|
|
107
|
+
};
|
|
74
108
|
},
|
|
75
109
|
rebuildFrom(entries) {
|
|
76
110
|
let rebuilt: DevsysState = initialState();
|