@gtrabanco/pi-agentic-workflow 0.24.4 → 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 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 = {
@@ -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
+ }
@@ -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;
@@ -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.24.4",
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.6.0"
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: execute-phase
3
3
  user-invocable: true
4
- version: 6.0.0
4
+ version: 6.1.0
5
5
  argument-hint: <NN-slug> [P<k>|step] [--max-attempts N] [--force]
6
6
  allowed-tools: [Bash, Read, Edit, Write, MultiEdit]
7
7
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
@@ -15,7 +15,12 @@ when every step below ran — fixing the code and stopping is the classic way
15
15
  findings end up "solved" locally but absent from the merged PR:
16
16
 
17
17
  ```
18
- ✓ Fixes implemented (scope: only the routed findings — nothing extra)
18
+ ✓ Fixes implemented — the batch is the routed findings **plus everything that
19
+ depends on them**: callers of what changes, consumers of what it delivers, and
20
+ every mention that stops being true (a comment, a prose claim, a declared
21
+ validator, a manifest narration). Nothing beyond that
22
+ ✓ `bun scripts/promise-coherence.mjs` RUN → `verdict PASS` (the repaired claims
23
+ reproduce at the head; a skipped line is read, never assumed)
19
24
  ✓ Gate RUN and green (exit codes pasted)
20
25
  ✓ Never edit an existing test's expectation to match behaviour — a setup
21
26
  repair keeps assertions at least as strong and never touches expectations
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: init-workspace
3
3
  user-invocable: true
4
- version: 4.0.2
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 without an explicit yes
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` is committed; `project.local.yml`
126
- and `cache` stay local. Never write `.serena/` config without consent. LSP
127
- never verifies documents — false positives happen when a file type's server
128
- is not listed (markdown/yaml/json are never auto-detected), and a listed
129
- server that fails to start must be removed (one failure disables every
130
- server in the project). Document verification is a script's job.
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
@@ -152,9 +157,9 @@ loads exactly upgrade and skips bootstrap plus portability.
152
157
 
153
158
  The adaptive lane (feature 61) replaces the fixed pipeline with a single-unit-
154
159
  document model. Every NEW unit produces one `SPEC.md` under
155
- `docs/features/<NN>-<slug>/` with 14 sections: Objective, Why, User outcome, Impact,
156
- Acceptance criteria, Non-goals, Future cost, Applicable tests, Known pre-existing
157
- issues, Tasks, Evidence, Progress log, Next, References.
160
+ `docs/features/<NN>-<slug>/` with 14 sections: Objective, Why, User outcome, Acceptance criteria, Impact,
161
+ Non-goals, Future cost, Applicable tests, Known pre-existing issues, Tasks,
162
+ Evidence, Progress log, Next, References.
158
163
 
159
164
  Conventions written into the project's AGENTS.md:
160
165
 
@@ -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
- - The Serena project config is provisioned (or reported as a residual with the
221
- reason, and the health check ran).
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 the Serena project config.** Load
28
- [SERENA](SERENA.md), ask consent, then: (a) append the `.gitignore`
29
- block additively (skip when already present); (b) write `.serena/project.yml`
30
- with the `language_servers` list for every detected language (never overwrite
31
- an existing file — report it); (c) run the health check from SERENA.md and
32
- record the result. If Serena is not installed, offer install or record as a
33
- residual.
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
- ## Tool install is a different step
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
- This file owns the **project config**, not the tool. If Serena is not installed,
127
- offer the install (ask-first; `uv`/`uvx` preferred, `pipx` fallback) or record
128
- it as a residual. The install half is tracked by #219.
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 → warn once, continue, and record the absence in the unit's
133
- evidence. **Never block a run on it.**
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 the Serena project config, additively only (Refs #219).** Load
96
- [SERENA](SERENA.md). Append the `.gitignore` block only if the target
97
- does not already contain it. Write `.serena/project.yml` only if it is
98
- absent — never touch an existing one. Report what was skipped and why.
99
- Run the health check and record the result. If Serena is not installed,
100
- offer install or record as a residual. This step is additive-only; it
101
- never rewrites or deletes, consistent with upgrade mode's rule.
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
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: orchestration-envelope
3
3
  user-invocable: false
4
- version: 2.3.0
4
+ version: 2.4.0
5
5
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
6
6
  license: MIT
7
7
  description: >
@@ -16,6 +16,14 @@
16
16
  ✓ 11. No reconstruction from memory — missing reference → STOP.
17
17
  ```
18
18
 
19
+ **A box that runs a command discloses what the command needs.** When a box
20
+ invokes a script with a placeholder (`node scripts/x.mjs <unit-dir>`), the same
21
+ skill's text must name the artifact that placeholder requires — the script
22
+ declares it (`PREREQUISITES` in `scripts/needs-input.mjs`) and
23
+ `scripts/promise-coherence.mjs` checks the disclosure. A box that instructs a
24
+ run the skill never equips reproduces the failure it was written to prevent
25
+ (fix #258 F45/F53: `missing-unit-doc`).
26
+
19
27
  **Single owner:** orchestration-envelope/references/TURN_CONTRACT.md. All skills load this + skill-specific additions only. Duplication forbidden. Missing reference = STOP.
20
28
 
21
29
  **Push policy supplement:** Box 4 covers the end-of-unit push. Some consumers (e.g. `execute-phase/references/PREFLIGHT.md`) define additional push rules for mid-phase commits when a PR is already open — those per-consumer supplements extend this contract and must be loaded alongside it by the consumer skill.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pre-execution-review
3
3
  user-invocable: false
4
- version: 2.7.1
4
+ version: 2.8.0
5
5
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
6
6
  license: MIT
7
7
  description: >
@@ -55,7 +55,7 @@ task | implementation-owner | validator | required-evidence | status
55
55
  | `authority-source` | the SPEC/acceptance/criterion or ledger row that *requires* it — never the implementation |
56
56
  | `phase` / `task` | exactly one phase and one task; a row needing two phases means the phase cut is wrong |
57
57
  | `implementation-owner` | the skill/role that performs it (usually `execute-phase`) |
58
- | `validator` | the command or check that proves it, copied from the acceptance artifact (`ACCEPTANCE.md` for legacy (pre-feature 61) units; `sed -n '/^## Acceptance criteria/,/^## /p' <unit>/SPEC.md \| git hash-object --stdin` for post-feature 61) or the phase's done-when |
58
+ | `validator` | the command or check that proves it, copied from the acceptance artifact (`ACCEPTANCE.md` for legacy (pre-feature 61) units; `sed -n '/^## Acceptance criteria/,/^## /p' <unit>/SPEC.md \| git hash-object --stdin` for post-feature 61) or the phase's done-when. When it is a backticked reader command, state the outcome it must produce — `` … matches <N> `` or `` … exits <N> `` — because `scripts/promise-coherence.mjs` re-runs it at the head and `verified` is only truthful while it reproduces. A validator with no stated outcome is a claim nobody can check |
59
59
  | `required-evidence` | what must be recorded where (test name, receipt line, ledger row) |
60
60
  | `status` | `planned \| in-progress \| verified \| n/a \| deferred` |
61
61
 
@@ -66,7 +66,9 @@ Status lifecycle and its hard edges:
66
66
  follow-up issue.
67
67
  - `verified` requires the named validator to have run and its evidence to be
68
68
  recorded where `required-evidence` says. An assertion of intent is not a
69
- status change.
69
+ status change. A count or exit code in the validator cell is re-run by
70
+ `scripts/promise-coherence.mjs`; when the command stops producing it, the row
71
+ is stale even while it still reads `verified`.
70
72
  - `n/a` needs evidence in the row (a `path:line`, ledger row, or cited contract)
71
73
  and may never contradict the SPEC's scope section. `n/a` without evidence is
72
74
  an open obligation.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: unit-lane
3
3
  user-invocable: true
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  argument-hint: <NN-slug | "<idea>"> [--retriage] | --fix <issue-number> | --from-issue <issue-number>
6
6
  description: >
7
7
  One-shot lane conductor for a delivery unit: triage, implement, evidence, and
@@ -43,6 +43,12 @@ its work landed in the unit doc before continuing:
43
43
  - [ ] Each executor turn recorded its Evidence row and dated Progress entry
44
44
  - [ ] Diff guard run at the lane's resume (see step 4 in SKILL.md)
45
45
  - [ ] `git status --porcelain` is empty when the lane resumes
46
+ - [ ] Each phase swept the change **plus everything that depends on it** —
47
+ callers of what changed, consumers of what it delivers, and every mention
48
+ that stops being true; a surviving mention is part of the same phase, not
49
+ a finding for the next review
50
+ - [ ] `bun scripts/promise-coherence.mjs` RUN at the lane's resume → `verdict
51
+ PASS` (the unit doc's declared validators reproduce at the head)
46
52
 
47
53
  ## Allowed
48
54
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: verification-contract
3
3
  user-invocable: false
4
- version: 1.4.2
4
+ version: 1.4.3
5
5
  author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
6
6
  license: MIT
7
7
  description: >
@@ -17,6 +17,16 @@ description: >
17
17
  Single owner of the delivery finish line. Planning freezes it; executors may
18
18
  strengthen coverage but cannot move it; review checks the same bytes.
19
19
 
20
+ ## Executable claims
21
+
22
+ An acceptance criterion or Evidence row that states a count, an exit code or a
23
+ length is a promise, not a description: `scripts/promise-coherence.mjs` re-runs
24
+ it. Write the outcome a command must produce (`` `cmd` matches <N> ``,
25
+ `` `cmd` exits <N> ``) or, for a shape promise, name the file and the
26
+ `## Section` the count is about (`` … a 3–5 line `## Inputs` in
27
+ `skills/<name>/SKILL.md` ``) — a length with no named surface cannot be recounted
28
+ and fails closed.
29
+
20
30
  ## Artifact
21
31
 
22
32
  Legacy (pre-feature 61) units (ACCEPTANCE.md present — binding if present):