@gtrabanco/pi-agentic-workflow 0.26.0 → 0.30.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.
- package/README.md +41 -0
- package/dist/extension/factory.d.ts +6 -0
- package/dist/extension/factory.js +9 -0
- package/dist/extension/index.js +19 -1
- package/dist/routing/serena-readiness.d.ts +77 -0
- package/dist/routing/serena-readiness.js +142 -0
- package/dist/routing/state.d.ts +32 -0
- package/dist/routing/state.js +51 -0
- package/package.json +5 -3
- package/skills/init-workspace/SKILL.md +18 -10
- package/skills/init-workspace/references/BOOTSTRAP_DISCOVERY.md +6 -0
- package/skills/init-workspace/references/BOOTSTRAP_WRITE.md +10 -7
- package/skills/init-workspace/references/SERENA.md +92 -6
- package/skills/init-workspace/references/UPGRADE.md +9 -7
package/README.md
CHANGED
|
@@ -146,6 +146,47 @@ set of routes, and you can define as many as you like under `profiles`:
|
|
|
146
146
|
The built-in `nan` profile is shown but not editable — create a profile to
|
|
147
147
|
override it.
|
|
148
148
|
|
|
149
|
+
## Serena (ships by default)
|
|
150
|
+
|
|
151
|
+
Installing this package also gives you **Serena's language-server tools** —
|
|
152
|
+
`serena_find_symbol`, `serena_find_referencing_symbols`,
|
|
153
|
+
`serena_get_symbols_overview`, `serena_find_declaration`, diagnostics and the
|
|
154
|
+
rest. Two declarations make that work, and they ship together: the exact-pinned
|
|
155
|
+
`@bacnh85/pi-serena` dependency, and a second `pi.extensions` entry pointing at
|
|
156
|
+
that dependency's own extension. Pi installs a package's dependencies but
|
|
157
|
+
activates only the package's own `pi.extensions` entries, so the pin alone would
|
|
158
|
+
install a toolset nothing ever loaded.
|
|
159
|
+
|
|
160
|
+
**You must run `/init-workspace` once per project.** This package never writes or
|
|
161
|
+
repairs `.serena/project.yml` — `init-workspace` is its only writer, so there is
|
|
162
|
+
one implementation of that contract and no drift. A project whose config is
|
|
163
|
+
missing, lists no `language_servers`, or was provisioned by an older bundled
|
|
164
|
+
`init-workspace` gets **one** notification per session state pointing at
|
|
165
|
+
`/init-workspace`; the latch lives in this package's state file, so a restart does
|
|
166
|
+
not re-nag.
|
|
167
|
+
|
|
168
|
+
Serena itself is a local tool: install the machine half with `uv tool install -p
|
|
169
|
+
3.13 serena-agent` (or `/init-workspace`, which offers it). Nothing is uploaded
|
|
170
|
+
and no key is configured.
|
|
171
|
+
|
|
172
|
+
**Switching it off.** pi owns package resources, so the off switch is pi's, not a
|
|
173
|
+
key of ours: `pi config` opens the resource picker (tab switches scope) and lets
|
|
174
|
+
you disable the package's resources, and the same filter is expressible in
|
|
175
|
+
settings under `packages`:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"packages": [
|
|
180
|
+
{ "source": "npm:@gtrabanco/pi-agentic-workflow", "extensions": ["./dist/extension/index.js"] }
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
That keeps the workflow's own extension and commands and drops the Serena
|
|
186
|
+
extension entry, so the `serena_*` tools are never registered. There is no
|
|
187
|
+
settings key inside this package for it: the entry is declared in the manifest,
|
|
188
|
+
and a config key cannot gate a manifest-declared extension.
|
|
189
|
+
|
|
149
190
|
## Path protection (optional)
|
|
150
191
|
|
|
151
192
|
The Tier 2 preventive guard blocks a `write` / `edit` tool call to an existing
|
|
@@ -43,6 +43,12 @@ export interface ExtensionDeps<M extends ModelRef = ModelRef> {
|
|
|
43
43
|
loadConfig?: (ctx: InvocationContext<M>) => LoadedConfig;
|
|
44
44
|
/** P4: profile demotion state store (optional — defaults to file-backed). */
|
|
45
45
|
profileState?: import("../routing/state.js").ProfileStateStore;
|
|
46
|
+
/**
|
|
47
|
+
* Feature 67 D5/AC9: the Serena readiness nudge. Consulted at the first
|
|
48
|
+
* workflow-command dispatch, so a session that never starts through
|
|
49
|
+
* `session_start` still hears it once.
|
|
50
|
+
*/
|
|
51
|
+
serenaReadiness?: import("../routing/serena-readiness.js").SerenaReadinessGate;
|
|
46
52
|
}
|
|
47
53
|
export interface ExtensionHandle<M extends ModelRef = ModelRef> {
|
|
48
54
|
router: Router<M>;
|
|
@@ -31,11 +31,19 @@ export function createExtension(deps) {
|
|
|
31
31
|
// P4: build the profile state store when not provided.
|
|
32
32
|
const profileState = deps.profileState ?? createProfileStateStore({ path: stateFilePath(agentDir) });
|
|
33
33
|
const router = createRouter({ surface, loadConfig: read, hint, settingsCommand: SETTINGS_COMMAND, knownCommands, profileState });
|
|
34
|
+
// Said once per state, not once per command: the gate latches in memory for
|
|
35
|
+
// this session and in the package's state file for later ones.
|
|
36
|
+
const reportSerenaReadiness = (ctx) => {
|
|
37
|
+
const message = deps.serenaReadiness?.check(ctx.cwd);
|
|
38
|
+
if (message)
|
|
39
|
+
ctx.notify(message, "warning");
|
|
40
|
+
};
|
|
34
41
|
for (const command of catalogue.commands) {
|
|
35
42
|
registrar.registerCommand(command.name, {
|
|
36
43
|
...(command.description ? { description: command.description } : {}),
|
|
37
44
|
handler: async (args, ctx) => {
|
|
38
45
|
reportCatalogueIssues(ctx);
|
|
46
|
+
reportSerenaReadiness(ctx);
|
|
39
47
|
await router.dispatch(command, args, ctx);
|
|
40
48
|
},
|
|
41
49
|
});
|
|
@@ -44,6 +52,7 @@ export function createExtension(deps) {
|
|
|
44
52
|
// and the same two config files, never a separate route surface (OB-5, AC6).
|
|
45
53
|
const settingsHandler = async (_args, ctx) => {
|
|
46
54
|
reportCatalogueIssues(ctx);
|
|
55
|
+
reportSerenaReadiness(ctx);
|
|
47
56
|
// Bound for the console: two verbs and the session they act on, so it cannot
|
|
48
57
|
// reach for `settle` or `dispatch` by accident.
|
|
49
58
|
const routing = {
|
package/dist/extension/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import { evaluateToolCall, isProtectedPath, normalizeTarget } from "../config/pa
|
|
|
7
7
|
import { createExtension } from "./factory.js";
|
|
8
8
|
import { readGitStatusBounded } from "./receipt-guard.js";
|
|
9
9
|
import { createPickerComponent, PICKER_MAX_VISIBLE, pagedSelect } from "../settings/picker.js";
|
|
10
|
-
import { createHintStore, stateFilePath } from "../routing/state.js";
|
|
10
|
+
import { createHintStore, createSerenaReadinessStore, stateFilePath } from "../routing/state.js";
|
|
11
|
+
import { createSerenaReadinessGate, readInitWorkspaceRevision } from "../routing/serena-readiness.js";
|
|
11
12
|
import { runSettingsConsole } from "../settings/console.js";
|
|
12
13
|
import { readConfigFile, writeConfigFile } from "../settings/store.js";
|
|
13
14
|
import { SETTINGS_COMMAND } from "../routing/types.js";
|
|
@@ -174,6 +175,14 @@ function resolveExistingPrefix(target) {
|
|
|
174
175
|
export default function extension(pi) {
|
|
175
176
|
const agentDir = getAgentDir();
|
|
176
177
|
const hint = createHintStore({ path: stateFilePath(agentDir) });
|
|
178
|
+
// One gate for both entry points (AC9): session start says it first, and a
|
|
179
|
+
// session that begins by running a command says it there instead. The
|
|
180
|
+
// revision is the bundled init-workspace's own, so an upgrade that ships a
|
|
181
|
+
// newer provisioning contract can tell the operator the project predates it.
|
|
182
|
+
const serenaReadiness = createSerenaReadinessGate({
|
|
183
|
+
store: createSerenaReadinessStore({ path: stateFilePath(agentDir) }),
|
|
184
|
+
revision: readInitWorkspaceRevision(skillsDir),
|
|
185
|
+
});
|
|
177
186
|
// Said once per session: an override that was ignored (or a malformed file)
|
|
178
187
|
// is reported the first time the guard runs, never on every tool call.
|
|
179
188
|
let reportedDegradations = false;
|
|
@@ -190,6 +199,7 @@ export default function extension(pi) {
|
|
|
190
199
|
skillsDir,
|
|
191
200
|
agentDir,
|
|
192
201
|
hint,
|
|
202
|
+
serenaReadiness,
|
|
193
203
|
loadConfig: (ctx) => loadConfig({ agentDir, cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() }),
|
|
194
204
|
// The console edits the same files the dispatcher reads (SPEC S4, AC10).
|
|
195
205
|
settings: ({ ctx, catalogue: routed, routing }) => runSettingsConsole({
|
|
@@ -204,6 +214,14 @@ export default function extension(pi) {
|
|
|
204
214
|
writeFile: writeConfigFile,
|
|
205
215
|
}),
|
|
206
216
|
});
|
|
217
|
+
// The readiness nudge's first chance (AC9): a session that opens here hears
|
|
218
|
+
// it, and the gate latches so the command path never repeats it. Soft by
|
|
219
|
+
// construction — the gate swallows a broken read and a failed latch.
|
|
220
|
+
pi.on("session_start", (_event, ctx) => {
|
|
221
|
+
const message = serenaReadiness.check(ctx.cwd);
|
|
222
|
+
if (message)
|
|
223
|
+
ctx.ui.notify(message, "warning");
|
|
224
|
+
});
|
|
207
225
|
pi.on("model_select", (event) => router.noteModelSelect(event.model));
|
|
208
226
|
pi.on("thinking_level_select", (event) => router.noteThinkingLevelSelect(event.level));
|
|
209
227
|
// The inline-receipt path is impossible: a `gh pr comment` carrying a
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SerenaReadinessRecord, SerenaReadinessStore } from "./state.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serena readiness nudge (feature 67 P5, decision D5 / AC9).
|
|
4
|
+
*
|
|
5
|
+
* A pi user who installs this package gets Serena's tools, but the project
|
|
6
|
+
* config is `init-workspace`'s to write (AC6: one writer, no drift). So this is
|
|
7
|
+
* the only thing the package may do about a project that was never provisioned:
|
|
8
|
+
* **say so, once**, and point at the command that fixes it.
|
|
9
|
+
*
|
|
10
|
+
* The contract, in four lines:
|
|
11
|
+
* - it reads `.serena/project.yml` and reports `missing`, `stale`, `outdated`
|
|
12
|
+
* (the bundled init-workspace moved on) or `ready`;
|
|
13
|
+
* - it announces **exactly one** message per recorded state, then latches —
|
|
14
|
+
* the latch lives in the package's own global state file, so a restart does
|
|
15
|
+
* not re-nag (the first-run hint's contract);
|
|
16
|
+
* - it writes nothing to the project and nothing to any configuration;
|
|
17
|
+
* - every failure is soft: a broken read is `missing`, a failed latch is a
|
|
18
|
+
* nuisance, and neither ever throws into a command.
|
|
19
|
+
*/
|
|
20
|
+
/** The one command that owns Serena provisioning. */
|
|
21
|
+
export declare const INIT_WORKSPACE_COMMAND = "/init-workspace";
|
|
22
|
+
/** What a `.serena/project.yml` says about itself — only what this check reads. */
|
|
23
|
+
export interface SerenaConfig {
|
|
24
|
+
present: boolean;
|
|
25
|
+
/** The `language_servers` entries, in file order. Empty when absent or empty. */
|
|
26
|
+
languageServers: string[];
|
|
27
|
+
}
|
|
28
|
+
export type SerenaReadiness = {
|
|
29
|
+
kind: "missing";
|
|
30
|
+
} | {
|
|
31
|
+
kind: "stale";
|
|
32
|
+
detail: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "outdated";
|
|
35
|
+
recorded: string;
|
|
36
|
+
revision: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "ready";
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Read the two facts this check needs out of the project config. A tolerant
|
|
42
|
+
* line scanner, not a YAML parser: the file is written by `init-workspace`, and
|
|
43
|
+
* the only key read here is the `language_servers` list. Anything it cannot
|
|
44
|
+
* understand reads as "no servers", which is the fail-closed direction — the
|
|
45
|
+
* nudge fires rather than a broken config passing silently.
|
|
46
|
+
*/
|
|
47
|
+
export declare function inspectSerenaConfig(text: string | null | undefined): SerenaConfig;
|
|
48
|
+
/**
|
|
49
|
+
* Decide what this project needs. `recorded` is the latch's last announcement —
|
|
50
|
+
* a changed shipped revision is itself a trigger, so an upgraded package can
|
|
51
|
+
* tell the operator that the config predates it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function evaluateSerenaReadiness({ config, recorded, revision, }: {
|
|
54
|
+
config: SerenaConfig;
|
|
55
|
+
recorded?: SerenaReadinessRecord;
|
|
56
|
+
revision: string;
|
|
57
|
+
}): SerenaReadiness;
|
|
58
|
+
/** The one notification this module may produce, or null when there is nothing to say. */
|
|
59
|
+
export declare function serenaReadinessMessage(readiness: SerenaReadiness): string | null;
|
|
60
|
+
export interface SerenaReadinessGate {
|
|
61
|
+
/** The message to announce for this project, or null when nothing is due. */
|
|
62
|
+
check(projectKey: string): string | null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The gate the two entry points share (session start and the first command
|
|
66
|
+
* dispatch). One latch per extension instance, so a single session announces at
|
|
67
|
+
* most once; the persisted record keeps later sessions quiet.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSerenaReadinessGate({ store, revision, readProject, now, }: {
|
|
70
|
+
store: SerenaReadinessStore;
|
|
71
|
+
revision: string;
|
|
72
|
+
/** Injectable so the suite reads fixtures instead of a real project. */
|
|
73
|
+
readProject?: (projectKey: string) => string | null;
|
|
74
|
+
now?: () => string;
|
|
75
|
+
}): SerenaReadinessGate;
|
|
76
|
+
/** The bundled `init-workspace` revision, read from the shipped skill's frontmatter. */
|
|
77
|
+
export declare function readInitWorkspaceRevision(skillsDir: string): string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Serena readiness nudge (feature 67 P5, decision D5 / AC9).
|
|
5
|
+
*
|
|
6
|
+
* A pi user who installs this package gets Serena's tools, but the project
|
|
7
|
+
* config is `init-workspace`'s to write (AC6: one writer, no drift). So this is
|
|
8
|
+
* the only thing the package may do about a project that was never provisioned:
|
|
9
|
+
* **say so, once**, and point at the command that fixes it.
|
|
10
|
+
*
|
|
11
|
+
* The contract, in four lines:
|
|
12
|
+
* - it reads `.serena/project.yml` and reports `missing`, `stale`, `outdated`
|
|
13
|
+
* (the bundled init-workspace moved on) or `ready`;
|
|
14
|
+
* - it announces **exactly one** message per recorded state, then latches —
|
|
15
|
+
* the latch lives in the package's own global state file, so a restart does
|
|
16
|
+
* not re-nag (the first-run hint's contract);
|
|
17
|
+
* - it writes nothing to the project and nothing to any configuration;
|
|
18
|
+
* - every failure is soft: a broken read is `missing`, a failed latch is a
|
|
19
|
+
* nuisance, and neither ever throws into a command.
|
|
20
|
+
*/
|
|
21
|
+
/** The one command that owns Serena provisioning. */
|
|
22
|
+
export const INIT_WORKSPACE_COMMAND = "/init-workspace";
|
|
23
|
+
/**
|
|
24
|
+
* Read the two facts this check needs out of the project config. A tolerant
|
|
25
|
+
* line scanner, not a YAML parser: the file is written by `init-workspace`, and
|
|
26
|
+
* the only key read here is the `language_servers` list. Anything it cannot
|
|
27
|
+
* understand reads as "no servers", which is the fail-closed direction — the
|
|
28
|
+
* nudge fires rather than a broken config passing silently.
|
|
29
|
+
*/
|
|
30
|
+
export function inspectSerenaConfig(text) {
|
|
31
|
+
if (typeof text !== "string")
|
|
32
|
+
return { present: false, languageServers: [] };
|
|
33
|
+
const lines = text.split(/\r?\n/u);
|
|
34
|
+
const start = lines.findIndex((line) => /^language_servers:/u.test(line));
|
|
35
|
+
if (start === -1)
|
|
36
|
+
return { present: true, languageServers: [] };
|
|
37
|
+
// Inline form: `language_servers: [markdown, json]`
|
|
38
|
+
const inline = /^language_servers:\s*\[(.*)\]\s*$/u.exec(lines[start]);
|
|
39
|
+
if (inline) {
|
|
40
|
+
return {
|
|
41
|
+
present: true,
|
|
42
|
+
languageServers: inline[1]
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((entry) => entry.trim().replace(/^["']|["']$/gu, ""))
|
|
45
|
+
.filter(Boolean),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const languageServers = [];
|
|
49
|
+
for (const line of lines.slice(start + 1)) {
|
|
50
|
+
const item = /^\s+-\s*(.+?)\s*$/u.exec(line);
|
|
51
|
+
if (!item) {
|
|
52
|
+
// The list ended (a new top-level key) or the line is blank/comment.
|
|
53
|
+
if (line.trim() !== "" && !/^\s*#/u.test(line))
|
|
54
|
+
break;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
languageServers.push(item[1].replace(/^["']|["']$/gu, ""));
|
|
58
|
+
}
|
|
59
|
+
return { present: true, languageServers };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decide what this project needs. `recorded` is the latch's last announcement —
|
|
63
|
+
* a changed shipped revision is itself a trigger, so an upgraded package can
|
|
64
|
+
* tell the operator that the config predates it.
|
|
65
|
+
*/
|
|
66
|
+
export function evaluateSerenaReadiness({ config, recorded, revision, }) {
|
|
67
|
+
if (!config.present)
|
|
68
|
+
return { kind: "missing" };
|
|
69
|
+
if (config.languageServers.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
kind: "stale",
|
|
72
|
+
detail: "the project config lists no language_servers, so no server will start for it",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (recorded?.state === "ready" && recorded.revision !== revision) {
|
|
76
|
+
return { kind: "outdated", recorded: recorded.revision, revision };
|
|
77
|
+
}
|
|
78
|
+
return { kind: "ready" };
|
|
79
|
+
}
|
|
80
|
+
/** The one notification this module may produce, or null when there is nothing to say. */
|
|
81
|
+
export function serenaReadinessMessage(readiness) {
|
|
82
|
+
switch (readiness.kind) {
|
|
83
|
+
case "missing":
|
|
84
|
+
return (`pi-agentic-workflow: serena — this project has no .serena/project.yml, so no language server ` +
|
|
85
|
+
`will start for it. Run ${INIT_WORKSPACE_COMMAND} to provision it (this is said once).`);
|
|
86
|
+
case "stale":
|
|
87
|
+
return (`pi-agentic-workflow: serena — ${readiness.detail}. Run ${INIT_WORKSPACE_COMMAND} to rewrite it ` +
|
|
88
|
+
`(this is said once).`);
|
|
89
|
+
case "outdated":
|
|
90
|
+
return (`pi-agentic-workflow: serena — this project's config was provisioned by init-workspace ` +
|
|
91
|
+
`${readiness.recorded}, and this package ships ${readiness.revision}. Run ${INIT_WORKSPACE_COMMAND} ` +
|
|
92
|
+
`to bring it up to date (this is said once).`);
|
|
93
|
+
case "ready":
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The gate the two entry points share (session start and the first command
|
|
99
|
+
* dispatch). One latch per extension instance, so a single session announces at
|
|
100
|
+
* most once; the persisted record keeps later sessions quiet.
|
|
101
|
+
*/
|
|
102
|
+
export function createSerenaReadinessGate({ store, revision, readProject = (projectKey) => {
|
|
103
|
+
try {
|
|
104
|
+
return readFileSync(join(projectKey, ".serena", "project.yml"), "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}, now = () => new Date().toISOString(), }) {
|
|
110
|
+
let announced = false;
|
|
111
|
+
return {
|
|
112
|
+
check(projectKey) {
|
|
113
|
+
if (announced)
|
|
114
|
+
return null;
|
|
115
|
+
announced = true;
|
|
116
|
+
const config = inspectSerenaConfig(readProject(projectKey));
|
|
117
|
+
const recorded = store.record(projectKey);
|
|
118
|
+
const readiness = evaluateSerenaReadiness({ config, recorded, revision });
|
|
119
|
+
// The latch compares STATE KINDS, not just the presence of a record: a
|
|
120
|
+
// project that was ready and lost its config is a new thing to say, while
|
|
121
|
+
// a project still missing one is not.
|
|
122
|
+
const due = readiness.kind !== "ready" && recorded?.state !== readiness.kind;
|
|
123
|
+
// Latch second, and always: a ready project still stores its revision, so
|
|
124
|
+
// a later upgrade has a baseline to compare against.
|
|
125
|
+
store.latch(projectKey, { state: readiness.kind, revision, at: now() });
|
|
126
|
+
if (!due)
|
|
127
|
+
return null;
|
|
128
|
+
return serenaReadinessMessage(readiness);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** The bundled `init-workspace` revision, read from the shipped skill's frontmatter. */
|
|
133
|
+
export function readInitWorkspaceRevision(skillsDir) {
|
|
134
|
+
try {
|
|
135
|
+
const text = readFileSync(join(skillsDir, "init-workspace", "SKILL.md"), "utf8");
|
|
136
|
+
const match = /^version:\s*(\S+)\s*$/mu.exec(text.split("\n---", 1)[0] ?? "");
|
|
137
|
+
return match ? match[1] : "unknown";
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return "unknown";
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/routing/state.d.ts
CHANGED
|
@@ -37,3 +37,35 @@ export interface ProfileStateStoreOptions {
|
|
|
37
37
|
writeFile?: (path: string, text: string) => void;
|
|
38
38
|
}
|
|
39
39
|
export declare function createProfileStateStore({ path, readFile, writeFile, }: ProfileStateStoreOptions): ProfileStateStore;
|
|
40
|
+
/**
|
|
41
|
+
* What the operator was last told about one project's Serena config, keyed by
|
|
42
|
+
* the project root. The record is the nudge's whole memory: the state kind it
|
|
43
|
+
* announced and the init-workspace revision that was current when it did.
|
|
44
|
+
*/
|
|
45
|
+
export interface SerenaReadinessRecord {
|
|
46
|
+
/** The readiness kind announced (`missing`, `stale`, `outdated`, `ready`). */
|
|
47
|
+
state: string;
|
|
48
|
+
/** The bundled `init-workspace` revision in force at that moment. */
|
|
49
|
+
revision: string;
|
|
50
|
+
/** ISO-8601 timestamp of the announcement. */
|
|
51
|
+
at: string;
|
|
52
|
+
}
|
|
53
|
+
export interface SerenaReadinessStore {
|
|
54
|
+
/** The record for `projectKey`, or undefined when this project was never seen. */
|
|
55
|
+
record(projectKey: string): SerenaReadinessRecord | undefined;
|
|
56
|
+
/** Persist the record for `projectKey`. Returns false when the write failed. */
|
|
57
|
+
latch(projectKey: string, record: SerenaReadinessRecord): boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface SerenaReadinessStoreOptions {
|
|
60
|
+
path: string;
|
|
61
|
+
readFile?: (path: string) => string | null;
|
|
62
|
+
writeFile?: (path: string, text: string) => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The nudge's latch, in the package's own global state file beside the
|
|
66
|
+
* first-run hint and the profile demotion — never inside the project, and never
|
|
67
|
+
* inside any configuration. Read-merge-write throughout, so no other key in
|
|
68
|
+
* that file is disturbed; every failure is soft (a latch that cannot be
|
|
69
|
+
* persisted re-shows the nudge next session, which is a nuisance, not a fault).
|
|
70
|
+
*/
|
|
71
|
+
export declare function createSerenaReadinessStore({ path, readFile, writeFile, }: SerenaReadinessStoreOptions): SerenaReadinessStore;
|
package/dist/routing/state.js
CHANGED
|
@@ -129,3 +129,54 @@ export function createProfileStateStore({ path, readFile = readOrNull, writeFile
|
|
|
129
129
|
},
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The nudge's latch, in the package's own global state file beside the
|
|
134
|
+
* first-run hint and the profile demotion — never inside the project, and never
|
|
135
|
+
* inside any configuration. Read-merge-write throughout, so no other key in
|
|
136
|
+
* that file is disturbed; every failure is soft (a latch that cannot be
|
|
137
|
+
* persisted re-shows the nudge next session, which is a nuisance, not a fault).
|
|
138
|
+
*/
|
|
139
|
+
export function createSerenaReadinessStore({ path, readFile = readOrNull, writeFile = writeThrough, }) {
|
|
140
|
+
const readState = () => {
|
|
141
|
+
const text = readFile(path);
|
|
142
|
+
if (text === null || text.trim() === "")
|
|
143
|
+
return {};
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(text);
|
|
146
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Corrupt state: treat as empty, so the nudge is shown rather than lost.
|
|
151
|
+
}
|
|
152
|
+
return {};
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
record(projectKey) {
|
|
156
|
+
const raw = readState().serenaReadiness;
|
|
157
|
+
if (typeof raw !== "object" || raw === null)
|
|
158
|
+
return undefined;
|
|
159
|
+
const entry = raw[projectKey];
|
|
160
|
+
if (typeof entry !== "object" || entry === null)
|
|
161
|
+
return undefined;
|
|
162
|
+
const { state, revision, at } = entry;
|
|
163
|
+
if (typeof state !== "string" || typeof revision !== "string" || typeof at !== "string")
|
|
164
|
+
return undefined;
|
|
165
|
+
return { state, revision, at };
|
|
166
|
+
},
|
|
167
|
+
latch(projectKey, record) {
|
|
168
|
+
const state = readState();
|
|
169
|
+
const existing = state.serenaReadiness;
|
|
170
|
+
const perProject = typeof existing === "object" && existing !== null ? existing : {};
|
|
171
|
+
perProject[projectKey] = record;
|
|
172
|
+
state.serenaReadiness = perProject;
|
|
173
|
+
try {
|
|
174
|
+
writeFile(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-agentic-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"type": "module",
|
|
22
22
|
"pi": {
|
|
23
23
|
"extensions": [
|
|
24
|
-
"./dist/extension/index.js"
|
|
24
|
+
"./dist/extension/index.js",
|
|
25
|
+
"./node_modules/@bacnh85/pi-serena/extensions/index.ts"
|
|
25
26
|
],
|
|
26
27
|
"skills": [
|
|
27
28
|
"./skills"
|
|
@@ -57,8 +58,9 @@
|
|
|
57
58
|
"@earendil-works/pi-tui": "*"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
61
|
+
"@bacnh85/pi-serena": "0.10.0",
|
|
60
62
|
"@gtrabanco/agentic-workflow": "0.2.0",
|
|
61
|
-
"@gtrabanco/agentic-workflow-schema": "4.
|
|
63
|
+
"@gtrabanco/agentic-workflow-schema": "4.7.0"
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
64
66
|
"@types/node": "24.13.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: init-workspace
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 4.0
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
argument-hint: <target-dir>
|
|
6
6
|
author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
|
|
7
7
|
license: MIT
|
|
@@ -26,7 +26,8 @@ raw placeholders.
|
|
|
26
26
|
three-tier dispatch and stops with the dispatcher's `open-questions@1` block
|
|
27
27
|
— against a scratch unit folder when no unit exists yet (§Consent).
|
|
28
28
|
✓ The adapted scaffold and repository-state ledger are written (or the merge/abort decision was asked) and remaining placeholders are listed
|
|
29
|
-
✓ Nothing was installed or overwritten
|
|
29
|
+
✓ Nothing was installed, registered (MCP) or overwritten — including the global
|
|
30
|
+
Serena config — without an explicit yes
|
|
30
31
|
✓ Artifact language: explicit user instruction > the project's declared docs language > English. The CONVERSATION language never decides — a Spanish prompt still produces English PRs/issues/commits/SPECs unless one of the first two says otherwise
|
|
31
32
|
✓ The closing `→ Next:` block is printed as the ABSOLUTE last output
|
|
32
33
|
```
|
|
@@ -122,12 +123,16 @@ loads exactly upgrade and skips bootstrap plus portability.
|
|
|
122
123
|
on its own. **The urgency label seeding is additive-only the same way:**
|
|
123
124
|
create a missing `urgent`/`fix-next` label, never touch one that already
|
|
124
125
|
exists (name, color, or description a project already customized).
|
|
125
|
-
- **Serena convention.** `.serena/project.yml`
|
|
126
|
-
and `cache` stay local. Never write
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
- **Serena convention — tool and config, one consent question.** `.serena/project.yml`
|
|
127
|
+
is committed; `project.local.yml` and `cache` stay local. Never write
|
|
128
|
+
`.serena/` config, install the tool, register an MCP server or touch the
|
|
129
|
+
global `serena_config.yml` (`trusted_project_path_patterns`) without an
|
|
130
|
+
explicit yes — [SERENA](references/SERENA.md) owns the single question and
|
|
131
|
+
the fixed provisioning report. LSP never verifies documents — false positives
|
|
132
|
+
happen when a file type's server is not listed (markdown/yaml/json are never
|
|
133
|
+
auto-detected), a listed server that fails to start must be removed (one
|
|
134
|
+
failure disables every server in the project), and LSP evidence stays
|
|
135
|
+
TypeScript/JavaScript-only. Document verification is a script's job.
|
|
131
136
|
- **Retrieval index scaffold and git hooks are consent-gated (AC22).** The
|
|
132
137
|
index surface — the committed `.agentic-workflow/index.json` config plus the
|
|
133
138
|
three freshness hooks (`post-merge`, `post-checkout`, `post-rewrite`, all
|
|
@@ -217,8 +222,11 @@ OpenCode adapter does not load this fallback.
|
|
|
217
222
|
listed as a residual when its hook API/dependency was unavailable.
|
|
218
223
|
The adaptive lane conventions (unit document, triage, catalog steps, guards,
|
|
219
224
|
evidence, commit formats) are written into AGENTS.md.
|
|
220
|
-
-
|
|
221
|
-
reason,
|
|
225
|
+
- Serena is provisioned and reported: the one consent question was asked, the
|
|
226
|
+
project config written (or skipped with its reason), the tool installed and its
|
|
227
|
+
MCP server registered for the detected platform — or each refused/absent item
|
|
228
|
+
recorded as a residual with its reason — and the fixed provisioning report
|
|
229
|
+
states the health check per listed server. The run completes either way.
|
|
222
230
|
- **The closing `→ Next:` block is printed** (plus the offer to install the skills):
|
|
223
231
|
|
|
224
232
|
```
|
|
@@ -69,3 +69,9 @@
|
|
|
69
69
|
website itself.
|
|
70
70
|
- **Naming conventions**, **MCP servers**, and **Serena language servers** (see
|
|
71
71
|
[SERENA](SERENA.md) for the mapping), if any.
|
|
72
|
+
- **Serena footprint** — the platform whose MCP config is the one to register
|
|
73
|
+
(Claude Code, Cursor, VS Code/Copilot, Codex, JetBrains), the installers
|
|
74
|
+
actually present (`uv --version`, `pipx --version`), and whether the global
|
|
75
|
+
`serena_config.yml` already covers this project root under
|
|
76
|
+
`trusted_project_path_patterns`. The write step offers **one** consent
|
|
77
|
+
question from these three facts; it never asks a second time.
|
|
@@ -24,13 +24,16 @@
|
|
|
24
24
|
7. **Propagate the unit doc template.** Ensure
|
|
25
25
|
`docs/features/_TEMPLATE/SPEC.md` exists with all 14 sections sections.
|
|
26
26
|
This is the template used when creating new unit documents.
|
|
27
|
-
8. **Provision
|
|
28
|
-
[SERENA](SERENA.md)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
8. **Provision Serena — one consented question, then write.** Load
|
|
28
|
+
[SERENA](SERENA.md) and ask its single consent question; one yes covers the
|
|
29
|
+
tool install, the detected platform's MCP registration and the global trust
|
|
30
|
+
append, and a refusal installs, registers and writes **nothing**. On yes:
|
|
31
|
+
(a) append the `.gitignore` block additively (skip when already present);
|
|
32
|
+
(b) write `.serena/project.yml` with the `language_servers` list for every
|
|
33
|
+
detected language (never overwrite an existing file — report it); (c) run the
|
|
34
|
+
health check and render the fixed provisioning report. When no installer is
|
|
35
|
+
available, offer only what the machine can perform and record the residual
|
|
36
|
+
with its reason — the run completes either way.
|
|
34
37
|
9. **Install accepted agent safety hooks.** Keep the canonical policy and
|
|
35
38
|
fullauto wrapper at `.agentic-workflow/hooks/`. Seed the shipped
|
|
36
39
|
path-protection policy at `.agentic-workflow/path-policy.json` and its doc
|
|
@@ -93,6 +93,12 @@ check names the offending entry. Removing a failing entry (per the startup
|
|
|
93
93
|
rule below) is the correct remediation — never leave a dead entry in the file.
|
|
94
94
|
**Never claim LSP evidence for a run whose server never started.**
|
|
95
95
|
|
|
96
|
+
The report's `health:` line carries one entry per listed server. When the check
|
|
97
|
+
cannot run at all — no tool, no installer, or a refused consent, including a
|
|
98
|
+
refused **trust** append (§Trust) — it says so once, on that line, with the
|
|
99
|
+
reason, instead of reporting a pass: an absent check is a residual, never a
|
|
100
|
+
silent pass.
|
|
101
|
+
|
|
96
102
|
### Startup rule (critical)
|
|
97
103
|
|
|
98
104
|
> **A server that fails to start must not stay listed.** One failing entry
|
|
@@ -121,13 +127,93 @@ deterministic script's job regardless — LSP serves navigation and diagnostics,
|
|
|
121
127
|
not our document grammar. `node`+`npm` are required for json/yaml; `libicu` is
|
|
122
128
|
required for marksman.
|
|
123
129
|
|
|
124
|
-
##
|
|
130
|
+
## Provisioning the tool — one consented question
|
|
131
|
+
|
|
132
|
+
This file owns the **project config** *and* the tool that reads it. The install
|
|
133
|
+
half (`#219`) lands here as one step, never a second interview.
|
|
134
|
+
|
|
135
|
+
**Order inside the step (fixed):**
|
|
136
|
+
|
|
137
|
+
1. **Detect what can be performed** — `uv --version` first, then `pipx
|
|
138
|
+
--version`. Never offer a route the machine cannot run.
|
|
139
|
+
2. **Ask ONCE**, with the whole set in the question: the tool install, the MCP
|
|
140
|
+
registration for **the detected platform only**, and the global trust append
|
|
141
|
+
when the project root is not covered. One yes covers the set the question
|
|
142
|
+
names; a narrower yes narrows the set.
|
|
143
|
+
3. **On yes** — install, register, append trust, then report exactly what
|
|
144
|
+
happened (§Report).
|
|
145
|
+
4. **On no** — install nothing, register nothing, write nothing. Record the
|
|
146
|
+
absence as a residual with its reason and finish; the run still completes.
|
|
147
|
+
|
|
148
|
+
**Install routes, in the order tried:**
|
|
149
|
+
|
|
150
|
+
| Route | Command | When |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| preferred | `uv tool install -p 3.13 serena-agent`, then `serena init` | `uv` is on PATH |
|
|
153
|
+
| fallback | `pipx install serena-agent`, then `serena init` | `uv` absent, `pipx` present — **MCP path only** |
|
|
154
|
+
|
|
155
|
+
`serena init` creates the global `serena_config.yml` when absent and never
|
|
156
|
+
rewrites an existing one. `serena -V` prints `Serena <version>`: that is the
|
|
157
|
+
health reader, and the only proof the tool runs.
|
|
158
|
+
|
|
159
|
+
### MCP registration — the detected platform, its own shape
|
|
160
|
+
|
|
161
|
+
Register **one** server named `serena`, in the config file of the platform
|
|
162
|
+
Step 0 detected. Never write a cross-platform blob, and never write a second
|
|
163
|
+
platform's file "to be safe". A per-workspace file (`.vscode/mcp.json`,
|
|
164
|
+
`.cursor/mcp.json`) needs the same consent as the rest; a global one
|
|
165
|
+
(`~/.codex/config.toml`, the JetBrains `mcp.json`) is append-only — the one
|
|
166
|
+
entry, never another line.
|
|
167
|
+
|
|
168
|
+
| Detected | Where | Entry |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| Claude Code | `claude mcp add serena -- serena start-mcp-server --context claude-code --project "$(pwd)"` (user scope: `--scope user`, `--project-from-cwd`) | command `serena` |
|
|
171
|
+
| Copilot / VS Code | `.vscode/mcp.json` → `servers.serena` | `{"type":"stdio","command":"serena","args":["start-mcp-server","--context=vscode","--project","${workspaceFolder}"]}` |
|
|
172
|
+
| Cursor | `.cursor/mcp.json` → `mcpServers.serena` | `{"command":"serena","args":["start-mcp-server","--context=ide"]}` |
|
|
173
|
+
| JetBrains Copilot | the global `mcp.json` Copilot opens | `{"type":"stdio","command":"serena","args":["start-mcp-server","--context=jb-copilot-plugin"]}` |
|
|
174
|
+
| Codex | `~/.codex/config.toml` → `[mcp_servers.serena]` | `command = "serena"`, `args = ["start-mcp-server","--project-from-cwd","--context=codex"]` |
|
|
175
|
+
|
|
176
|
+
### Trust — the fresh clone (global config, consent-gated)
|
|
177
|
+
|
|
178
|
+
`trusted_project_path_patterns` lives in the user's **global** `serena_config.yml`,
|
|
179
|
+
not the project's `.serena/project.yml`, and a fresh config defaults it to `[]`.
|
|
180
|
+
A project root no pattern covers starts no language server, so the health check
|
|
181
|
+
fails with nothing in the project to blame.
|
|
182
|
+
|
|
183
|
+
- **Detect** — read the global config, test this project root against the
|
|
184
|
+
patterns.
|
|
185
|
+
- **Offer the append inside the same consented question.** This file is shared by
|
|
186
|
+
every project on the machine, so it is never written implicitly.
|
|
187
|
+
- **On yes** — append **one pattern naming this project root**. Never rewrite,
|
|
188
|
+
reorder or reformat any other line; an append is the only permitted edit.
|
|
189
|
+
- **On no** — record the untrusted state as the health-check residual. It is a
|
|
190
|
+
residual, not an error, and it never blocks the run.
|
|
191
|
+
|
|
192
|
+
### Report — exactly what happened
|
|
193
|
+
|
|
194
|
+
Close the step with this fixed block. "Installed" is never assumed and never
|
|
195
|
+
reported from the intent to install:
|
|
125
196
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
197
|
+
```text
|
|
198
|
+
SERENA PROVISIONING
|
|
199
|
+
tool: <installed | already present | refused | no installer | failed: <reason>>
|
|
200
|
+
version: <serena -V output, or n/a>
|
|
201
|
+
mcp: <registered <platform> -> <path> | already present | refused | failed: <reason>>
|
|
202
|
+
trust: <appended <pattern> | already covered | refused - untrusted | n/a>
|
|
203
|
+
config: <written .serena/project.yml | already present | skipped: <reason>>
|
|
204
|
+
health: <one line per listed server, see below>
|
|
205
|
+
residual: <none | the reason this run is degraded>
|
|
206
|
+
```
|
|
129
207
|
|
|
130
208
|
## Degraded path
|
|
131
209
|
|
|
132
|
-
Serena unavailable
|
|
133
|
-
|
|
210
|
+
Serena unavailable — no tool, no installer (`uv`, `pipx`), or a refused consent —
|
|
211
|
+
→ warn once, continue, and record the absence with its reason in the unit's
|
|
212
|
+
evidence. **Never block a run on it**, and never let a degraded run read as a
|
|
213
|
+
healthy one: the report says which line is missing and why.
|
|
214
|
+
|
|
215
|
+
LSP evidence itself stays confined to **TypeScript/JavaScript**: a diagnostic
|
|
216
|
+
from any other listed server is navigation aid, never verification evidence, and
|
|
217
|
+
no claim may cite it. Document verification (schema rules, required sections,
|
|
218
|
+
column counts) stays a **deterministic script's** job regardless — LSP serves
|
|
219
|
+
navigation, not our document grammar.
|
|
@@ -92,13 +92,15 @@ lacks**. Nine ordered steps:
|
|
|
92
92
|
rule as the doc blocks; a pre-existing `urgent`/`fix-next` label the
|
|
93
93
|
project recolored or redescribed is left exactly as-is).
|
|
94
94
|
|
|
95
|
-
8. **Provision
|
|
96
|
-
[SERENA](SERENA.md)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
8. **Provision Serena, additively only (Refs #219).** Load
|
|
96
|
+
[SERENA](SERENA.md) and ask its single consent question — the tool install,
|
|
97
|
+
the detected platform's MCP registration and the global trust append all ride
|
|
98
|
+
on that one yes, and a refusal writes nothing. Then append the `.gitignore`
|
|
99
|
+
block only if the target lacks it, and write `.serena/project.yml` only if it
|
|
100
|
+
is absent — never touch an existing one. Report what was skipped and why, run
|
|
101
|
+
the health check, and render the fixed provisioning report. This step is
|
|
102
|
+
additive-only; it never rewrites or deletes, consistent with upgrade mode's
|
|
103
|
+
rule.
|
|
102
104
|
9. **Report + hand off.** Summarize blocks added, filled, and skipped
|
|
103
105
|
(residuals), the legacy `CLAUDE.md` outcome (folded items and removed, or kept
|
|
104
106
|
with the reason), the urgency labels seeded (or already present), then print the
|