@gtrabanco/pi-agentic-workflow 0.9.0 → 0.9.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.
- package/README.es.md +5 -1
- package/README.md +5 -1
- package/dist/extension/index.d.ts +10 -1
- package/dist/extension/index.js +9 -3
- package/dist/settings/console.d.ts +1 -0
- package/dist/settings/console.js +70 -6
- package/dist/settings/picker.d.ts +50 -0
- package/dist/settings/picker.js +112 -2
- package/package.json +1 -1
- package/skills/workflow-status/SKILL.md +50 -75
- package/skills/workflow-status/references/ENVELOPE_CORE.md +17 -2
- package/skills/workflow-status/references/ENVELOPE_FIELDS.md +9 -0
- package/skills/workflow-status/references/SENSOR_CORE.md +9 -9
- package/skills/workflow-status/references/SENSOR_SIGNALS.md +8 -5
package/README.es.md
CHANGED
|
@@ -164,7 +164,11 @@ byte a byte idéntico; el campo de modelo puede construir una cadena de respaldo
|
|
|
164
164
|
ordenada (`a/m1 → b/m2`); y una sola pasada puede aplicar o limpiar una ruta en
|
|
165
165
|
varios comandos, avisando por comando cuando una referencia falta del registro
|
|
166
166
|
vivo. Fuera de una sesión TUI los selectores caen a un prompt simple, así que la
|
|
167
|
-
consola nunca se queda sin salida.
|
|
167
|
+
consola nunca se queda sin salida. Las listas de más de 24 opciones tampoco
|
|
168
|
+
llegan nunca a un solo diálogo: por encima del tope el selector de modelo
|
|
169
|
+
pregunta primero el proveedor, y una lista de proveedores o de modelos
|
|
170
|
+
demasiado larga pagina (`More options…` / `◀ Previous page`) en vez de tumbar
|
|
171
|
+
el diálogo de select del host.
|
|
168
172
|
|
|
169
173
|
## Diagnóstico
|
|
170
174
|
|
package/README.md
CHANGED
|
@@ -158,7 +158,11 @@ you are, and the value currently in force is pre-selected and labelled
|
|
|
158
158
|
model field can build an ordered fallback chain (`a/m1 → b/m2`); and one pass
|
|
159
159
|
can apply or clear a route across several commands, warning per command when a
|
|
160
160
|
reference is missing from the live registry. Outside a TUI session the pickers
|
|
161
|
-
fall back to a plain prompt, so the console never dead-ends.
|
|
161
|
+
fall back to a plain prompt, so the console never dead-ends. Lists longer than
|
|
162
|
+
24 options never reach a single dialog either: over the cap the model picker
|
|
163
|
+
asks for the provider first, and an overlong provider or model list pages
|
|
164
|
+
(`More options…` / `◀ Previous page`) instead of crashing the host's select
|
|
165
|
+
dialog.
|
|
162
166
|
|
|
163
167
|
## Troubleshooting
|
|
164
168
|
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SettingsUi } from "../routing/types.js";
|
|
2
3
|
import { SETTINGS_COMMAND } from "../routing/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The interactive slice the settings console sees: Pi's own `ui` plus the rich
|
|
6
|
+
* picker (P4). The picker is terminal-only, so a non-TUI mode (headless/RPC)
|
|
7
|
+
* gets the plain `select` path — the console never dead-ends (OB-12, PE-004).
|
|
8
|
+
* Exported as a test seam: the fallback's bounded paging (AC8) is exercised
|
|
9
|
+
* directly, without standing up the whole extension.
|
|
10
|
+
*/
|
|
11
|
+
export declare function richUi(ctx: ExtensionContext): SettingsUi;
|
|
3
12
|
export default function extension(pi: ExtensionAPI): void;
|
|
4
13
|
export { SETTINGS_COMMAND };
|
package/dist/extension/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { loadConfig } from "../config/load.js";
|
|
5
5
|
import { createExtension } from "./factory.js";
|
|
6
|
-
import { createPickerComponent, PICKER_MAX_VISIBLE } from "../settings/picker.js";
|
|
6
|
+
import { createPickerComponent, PICKER_MAX_VISIBLE, pagedSelect } from "../settings/picker.js";
|
|
7
7
|
import { createHintStore, stateFilePath } from "../routing/state.js";
|
|
8
8
|
import { runSettingsConsole } from "../settings/console.js";
|
|
9
9
|
import { readConfigFile, writeConfigFile } from "../settings/store.js";
|
|
@@ -25,8 +25,10 @@ const thinkingLevelsInSyncWithPi = true;
|
|
|
25
25
|
* The interactive slice the settings console sees: Pi's own `ui` plus the rich
|
|
26
26
|
* picker (P4). The picker is terminal-only, so a non-TUI mode (headless/RPC)
|
|
27
27
|
* gets the plain `select` path — the console never dead-ends (OB-12, PE-004).
|
|
28
|
+
* Exported as a test seam: the fallback's bounded paging (AC8) is exercised
|
|
29
|
+
* directly, without standing up the whole extension.
|
|
28
30
|
*/
|
|
29
|
-
function richUi(ctx) {
|
|
31
|
+
export function richUi(ctx) {
|
|
30
32
|
const base = ctx.ui;
|
|
31
33
|
return {
|
|
32
34
|
select: (title, options) => base.select(title, [...options]),
|
|
@@ -35,7 +37,10 @@ function richUi(ctx) {
|
|
|
35
37
|
notify: (message, kind) => base.notify(message, kind),
|
|
36
38
|
pick: async (title, options, opts = {}) => {
|
|
37
39
|
if (ctx.mode !== "tui") {
|
|
38
|
-
|
|
40
|
+
// Non-TUI: the host's `select` dialog carries the same 24-option cap, so
|
|
41
|
+
// an over-cap list pages instead of crashing `pick` (AC8, PE-002). A
|
|
42
|
+
// list within the cap is one call with the same options, in order.
|
|
43
|
+
const picked = await pagedSelect((selectTitle, selectOptions) => base.select(selectTitle, [...selectOptions]), title, [...options]);
|
|
39
44
|
if (opts.multiple)
|
|
40
45
|
return picked ? [picked] : undefined;
|
|
41
46
|
return picked;
|
|
@@ -53,6 +58,7 @@ function richUi(ctx) {
|
|
|
53
58
|
items,
|
|
54
59
|
maxVisible: PICKER_MAX_VISIBLE,
|
|
55
60
|
theme: selectListTheme,
|
|
61
|
+
initial: opts.initial,
|
|
56
62
|
onSelect: (value) => done(value),
|
|
57
63
|
onCancel: () => done(undefined),
|
|
58
64
|
});
|
|
@@ -55,6 +55,7 @@ export declare const prompts: {
|
|
|
55
55
|
readonly cancel: "Cancel";
|
|
56
56
|
readonly model: (target: string) => string;
|
|
57
57
|
readonly modelPicked: (target: string) => string;
|
|
58
|
+
readonly modelProvider: (target: string) => string;
|
|
58
59
|
readonly thinking: (target: string) => string;
|
|
59
60
|
readonly fields: "Which fields should change?";
|
|
60
61
|
readonly fieldsBoth: "model and thinking";
|
package/dist/settings/console.js
CHANGED
|
@@ -12,6 +12,7 @@ import { effectiveRoute } from "../config/merge.js";
|
|
|
12
12
|
import { parseConfigFile, parseModelReference } from "../config/schema.js";
|
|
13
13
|
import { MAX_MODEL_CHAIN, SETTLE_POLICIES, THINKING_LEVELS, UNAVAILABLE_ROUTE_POLICIES } from "../config/types.js";
|
|
14
14
|
import { renderMergedConfig, routePath, DEFAULT_ROUTE } from "./view.js";
|
|
15
|
+
import { SELECT_OPTION_LIMIT, pagedSelect, providerOf } from "./picker.js";
|
|
15
16
|
/** The console's questions. `test/settings-console.test.mjs` drives the flow
|
|
16
17
|
* through these strings, so renaming one fails the tests that use it rather than
|
|
17
18
|
* silently re-sequencing them. */
|
|
@@ -38,6 +39,7 @@ export const prompts = {
|
|
|
38
39
|
cancel: "Cancel",
|
|
39
40
|
model: (target) => `Model for ${target}?`,
|
|
40
41
|
modelPicked: (target) => `Which model for ${target}?`,
|
|
42
|
+
modelProvider: (target) => `Provider for ${target}?`,
|
|
41
43
|
thinking: (target) => `Thinking level for ${target}?`,
|
|
42
44
|
fields: "Which fields should change?",
|
|
43
45
|
fieldsBoth: "model and thinking",
|
|
@@ -225,18 +227,25 @@ async function askFields(deps) {
|
|
|
225
227
|
}
|
|
226
228
|
/** Ask for one model entry; `undefined` means cancelled/skipped, `"inherit"` means the whole-route setting. */
|
|
227
229
|
async function pickModelEntry(deps, target, current) {
|
|
230
|
+
const models = deps.models ?? [];
|
|
228
231
|
let answer;
|
|
229
|
-
if (
|
|
232
|
+
if (models.length > SELECT_OPTION_LIMIT - 1) {
|
|
233
|
+
// Over the cap: TYPED occupies one dialog slot, so 24 models + TYPED would
|
|
234
|
+
// still crash (Decision 1). The issue's provider-first two-step keeps every
|
|
235
|
+
// dialog bounded in every UI mode (Decision 3).
|
|
236
|
+
answer = await askModelOverCap(deps, target, models, current);
|
|
237
|
+
}
|
|
238
|
+
else if (typeof deps.ui.pick === "function" && models.length > 0) {
|
|
230
239
|
// Rich seam: filterable, windowed, preselected to the value in force (OB-1).
|
|
231
|
-
const picked = await deps.ui.pick(prompts.modelPicked(target), [...
|
|
240
|
+
const picked = await deps.ui.pick(prompts.modelPicked(target), [...models, TYPED], {
|
|
232
241
|
initial: typeof current === "string" ? current : undefined,
|
|
233
242
|
});
|
|
234
243
|
answer = typeof picked === "string" ? picked : undefined;
|
|
235
244
|
if (answer === TYPED || answer === undefined)
|
|
236
245
|
answer = await deps.ui.input(prompts.model(target), "provider/modelId or inherit");
|
|
237
246
|
}
|
|
238
|
-
else if (
|
|
239
|
-
answer = await deps.ui.select(prompts.modelPicked(target), [...
|
|
247
|
+
else if (models.length > 0) {
|
|
248
|
+
answer = await deps.ui.select(prompts.modelPicked(target), [...models, TYPED]);
|
|
240
249
|
if (answer === TYPED || answer === undefined)
|
|
241
250
|
answer = await deps.ui.input(prompts.model(target), "provider/modelId or inherit");
|
|
242
251
|
}
|
|
@@ -257,6 +266,60 @@ async function pickModelEntry(deps, target, current) {
|
|
|
257
266
|
}
|
|
258
267
|
return `${parts.provider}/${parts.id}`;
|
|
259
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* The provider-first two-step the issue prescribes when the live registry exceeds
|
|
271
|
+
* the single-dialog cap (PE-003, PE-014). Unique providers (prefix before the
|
|
272
|
+
* first `/`, `localeCompare`-sorted) come first — through the rich picker while
|
|
273
|
+
* it fits, `pagedSelect` otherwise — then that provider's models in registry
|
|
274
|
+
* order. "Type another reference…" is the trailing entry of every dialog and
|
|
275
|
+
* still reaches the text input, so a pick-less UI completes the flow (OB-12).
|
|
276
|
+
*/
|
|
277
|
+
async function askModelOverCap(deps, target, models, current) {
|
|
278
|
+
const currentProvider = typeof current === "string" ? providerOf(current) : undefined;
|
|
279
|
+
const providers = [...new Set(models.map((model) => providerOf(model)))].sort((a, b) => a.localeCompare(b));
|
|
280
|
+
const provider = await askProvider(deps, target, providers, currentProvider);
|
|
281
|
+
if (provider === TYPED || provider === undefined) {
|
|
282
|
+
return await deps.ui.input(prompts.model(target), "provider/modelId or inherit");
|
|
283
|
+
}
|
|
284
|
+
const providerModels = models.filter((model) => providerOf(model) === provider);
|
|
285
|
+
const picked = await askModelWithinProvider(deps, target, providerModels, provider, current);
|
|
286
|
+
if (picked === TYPED || picked === undefined) {
|
|
287
|
+
return await deps.ui.input(prompts.model(target), "provider/modelId or inherit");
|
|
288
|
+
}
|
|
289
|
+
return picked;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Ask which provider. The rich picker handles it while `providers + TYPED` fits
|
|
293
|
+
* the cap (preselected to the value in force's provider); over the cap — Pi
|
|
294
|
+
* imposes no provider ceiling, so a > 24-provider registry is real (PE-014) —
|
|
295
|
+
* the dialog pages instead of crashing.
|
|
296
|
+
*/
|
|
297
|
+
async function askProvider(deps, target, providers, currentProvider) {
|
|
298
|
+
const title = prompts.modelProvider(target);
|
|
299
|
+
if (typeof deps.ui.pick === "function" && providers.length + 1 <= SELECT_OPTION_LIMIT) {
|
|
300
|
+
const picked = await deps.ui.pick(title, [...providers, TYPED], {
|
|
301
|
+
...(currentProvider !== undefined ? { initial: currentProvider } : {}),
|
|
302
|
+
});
|
|
303
|
+
return typeof picked === "string" ? picked : undefined;
|
|
304
|
+
}
|
|
305
|
+
return pagedSelect((selectTitle, options) => deps.ui.select(selectTitle, options), title, providers, { trailing: TYPED });
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Ask the model within one provider. The rich picker handles it while `models +
|
|
309
|
+
* TYPED` fits the cap (preselected only when the value in force belongs to this
|
|
310
|
+
* provider); over the cap the dialog pages.
|
|
311
|
+
*/
|
|
312
|
+
async function askModelWithinProvider(deps, target, models, provider, current) {
|
|
313
|
+
const title = prompts.modelPicked(target);
|
|
314
|
+
if (typeof deps.ui.pick === "function" && models.length + 1 <= SELECT_OPTION_LIMIT) {
|
|
315
|
+
const initial = typeof current === "string" && providerOf(current) === provider ? current : undefined;
|
|
316
|
+
const picked = await deps.ui.pick(title, [...models, TYPED], {
|
|
317
|
+
...(initial !== undefined ? { initial } : {}),
|
|
318
|
+
});
|
|
319
|
+
return typeof picked === "string" ? picked : undefined;
|
|
320
|
+
}
|
|
321
|
+
return pagedSelect((selectTitle, options) => deps.ui.select(selectTitle, options), title, models, { trailing: TYPED });
|
|
322
|
+
}
|
|
260
323
|
/** Ask for a model; a lone reference is returned as-is, several references are returned as an ordered chain. */
|
|
261
324
|
async function askModel(deps, target, current) {
|
|
262
325
|
// The value in force (OB-3). For a route whose model is already a chain, seed
|
|
@@ -334,7 +397,8 @@ async function pickCommand(deps, options) {
|
|
|
334
397
|
deps.ui.notify("There is no command to pick here.", "warning");
|
|
335
398
|
return undefined;
|
|
336
399
|
}
|
|
337
|
-
|
|
400
|
+
const sorted = [...options].sort((a, b) => a.localeCompare(b));
|
|
401
|
+
return pagedSelect((title, list) => deps.ui.select(title, list), prompts.command, sorted);
|
|
338
402
|
}
|
|
339
403
|
/** Multi-select command picker over the seam's `multiple` mode; a non-rich UI falls back to repeated single selects. */
|
|
340
404
|
async function pickCommandsMulti(deps, options) {
|
|
@@ -355,7 +419,7 @@ async function pickCommandsMulti(deps, options) {
|
|
|
355
419
|
const remaining = sorted.filter((option) => !chosen.includes(option));
|
|
356
420
|
if (remaining.length === 0)
|
|
357
421
|
break;
|
|
358
|
-
const picked = await deps.ui.select(prompts.command, remaining);
|
|
422
|
+
const picked = await pagedSelect((title, list) => deps.ui.select(title, list), prompts.command, remaining);
|
|
359
423
|
if (picked === undefined)
|
|
360
424
|
break;
|
|
361
425
|
chosen.push(picked);
|
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
import { type Component, type SelectItem, type SelectListTheme } from "@earendil-works/pi-tui";
|
|
2
2
|
/** Default window height for the SelectList (the built-in position indicator scales to the list). */
|
|
3
3
|
export declare const PICKER_MAX_VISIBLE = 10;
|
|
4
|
+
/**
|
|
5
|
+
* The host dialog cap this package must never exceed (PE-001): pi-web rejects a
|
|
6
|
+
* `select` dialog offering more than 24 options with "A select dialog must not
|
|
7
|
+
* offer more than 24 options". It is the single knob if a future host enforces a
|
|
8
|
+
* different bound.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SELECT_OPTION_LIMIT = 24;
|
|
11
|
+
/** Pager entry offered on every page but the first. */
|
|
12
|
+
export declare const PAGED_SELECT_PREV = "\u25C0 Previous page";
|
|
13
|
+
/** Pager entry offered on every page but the last. */
|
|
14
|
+
export declare const PAGED_SELECT_NEXT = "More options\u2026";
|
|
15
|
+
/** The plain-dialog shape `pagedSelect` drives (Pi's `ctx.ui.select`). */
|
|
16
|
+
export type SelectFn = (title: string, options: readonly string[]) => string | undefined | Promise<string | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Present `options` through `select` without ever offering more than
|
|
19
|
+
* `SELECT_OPTION_LIMIT` entries in one dialog (AC6, PE-001, PE-012).
|
|
20
|
+
*
|
|
21
|
+
* - A list that fits — data plus `trailing` — opens exactly one dialog, in the
|
|
22
|
+
* caller's order with `trailing` last, so a ≤ cap flow is byte-identical to a
|
|
23
|
+
* plain `select` call.
|
|
24
|
+
* - A longer list pages `PAGED_SELECT_PAGE_SIZE` data entries at a time —
|
|
25
|
+
* `SELECT_OPTION_LIMIT - 3`, so the derived page never hardcodes the cap —
|
|
26
|
+
* appending `PAGED_SELECT_PREV` and/or `PAGED_SELECT_NEXT` before `trailing`.
|
|
27
|
+
* Pager entries navigate; `trailing` and data entries are returned to the
|
|
28
|
+
* caller; `undefined` (cancel) passes straight through.
|
|
29
|
+
*
|
|
30
|
+
* The two pager labels are reserved: a data value (or a `trailing` value) that
|
|
31
|
+
* spells one is indistinguishable from the pager entry in the string `select`
|
|
32
|
+
* answers with, so a colliding pager label is decorated until it names nothing
|
|
33
|
+
* else on the dialog — the data value stays selectable and so does the pager.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pagedSelect(select: SelectFn, title: string, options: readonly string[], { trailing }?: {
|
|
36
|
+
trailing?: string;
|
|
37
|
+
}): Promise<string | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* The provider prefix Pi resolves a reference by — everything before the first
|
|
40
|
+
* `/` (SPEC Decision 6).
|
|
41
|
+
*
|
|
42
|
+
* Lenient on purpose, and deliberately distinct from the strict
|
|
43
|
+
* `parseModelReference` in `../config/schema.js`, which validates and rejects:
|
|
44
|
+
* this split must answer for any registry string. Both the console's
|
|
45
|
+
* provider-first grouping and the slash-aware filter below take their provider
|
|
46
|
+
* from here, so the two settings-side consumers cannot drift apart.
|
|
47
|
+
*/
|
|
48
|
+
export declare function providerOf(reference: string): string;
|
|
4
49
|
/**
|
|
5
50
|
* Token/subsequence, slash-aware reference filter (OB-2).
|
|
6
51
|
*
|
|
@@ -19,11 +64,16 @@ export declare function filterReferences(query: string, refs: readonly string[])
|
|
|
19
64
|
* `filterReferences` as the operator types so the subsequence/slash-aware
|
|
20
65
|
* filter (OB-2) — not `SelectList`'s own prefix `setFilter` — drives what the
|
|
21
66
|
* window shows. Control keys (arrows, Enter, Escape) forward to the list.
|
|
67
|
+
*
|
|
68
|
+
* `initial` — the value in force — is selected in the window the picker opens
|
|
69
|
+
* with, through the SelectList's own `setSelectedIndex` (OB-3).
|
|
22
70
|
*/
|
|
23
71
|
export declare function createPickerComponent(options: {
|
|
24
72
|
items: readonly SelectItem[];
|
|
25
73
|
maxVisible: number;
|
|
26
74
|
theme: SelectListTheme;
|
|
75
|
+
/** The value in force, selected in the window the picker opens with (OB-3). */
|
|
76
|
+
initial?: string;
|
|
27
77
|
onSelect: (value: string) => void;
|
|
28
78
|
onCancel?: () => void;
|
|
29
79
|
}): Component & {
|
package/dist/settings/picker.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// to narrow the list, selection, and cancel. The Pi-free `SettingsUi.pick`
|
|
11
11
|
// seam stays separated so the filter and the component are exercisable
|
|
12
12
|
// without a live session.
|
|
13
|
+
// - `pagedSelect` is the plain-`select` counterpart: the host's dialog cap
|
|
14
|
+
// (pi-web rejects > 24 options, PE-001) is never a picker concern — a UI
|
|
15
|
+
// without the rich seam still gets a bounded dialog, and an over-cap list
|
|
16
|
+
// pages instead of crashing.
|
|
13
17
|
//
|
|
14
18
|
// The filter is implemented in this package (not imported from Pi): PE-004
|
|
15
19
|
// confirmed pi-coding-agent's `fuzzyFilter` is internal, so the same
|
|
@@ -17,6 +21,99 @@
|
|
|
17
21
|
import { SelectList } from "@earendil-works/pi-tui";
|
|
18
22
|
/** Default window height for the SelectList (the built-in position indicator scales to the list). */
|
|
19
23
|
export const PICKER_MAX_VISIBLE = 10;
|
|
24
|
+
/**
|
|
25
|
+
* The host dialog cap this package must never exceed (PE-001): pi-web rejects a
|
|
26
|
+
* `select` dialog offering more than 24 options with "A select dialog must not
|
|
27
|
+
* offer more than 24 options". It is the single knob if a future host enforces a
|
|
28
|
+
* different bound.
|
|
29
|
+
*/
|
|
30
|
+
export const SELECT_OPTION_LIMIT = 24;
|
|
31
|
+
/** Pager entry offered on every page but the first. */
|
|
32
|
+
export const PAGED_SELECT_PREV = "◀ Previous page";
|
|
33
|
+
/** Pager entry offered on every page but the last. */
|
|
34
|
+
export const PAGED_SELECT_NEXT = "More options…";
|
|
35
|
+
/**
|
|
36
|
+
* Data entries per page: `SELECT_OPTION_LIMIT - 3` — room inside one dialog for
|
|
37
|
+
* both pager entries and the caller's trailing option.
|
|
38
|
+
*/
|
|
39
|
+
const PAGED_SELECT_PAGE_SIZE = SELECT_OPTION_LIMIT - 3;
|
|
40
|
+
/**
|
|
41
|
+
* The pager label actually offered: `base`, decorated with `marker` while some
|
|
42
|
+
* value in the dialog spells it, so a label-colliding data entry can never be
|
|
43
|
+
* swallowed by the navigation guard (see the contract above).
|
|
44
|
+
*/
|
|
45
|
+
function pagerLabel(base, marker, spoken) {
|
|
46
|
+
let label = base;
|
|
47
|
+
while (spoken.includes(label))
|
|
48
|
+
label += marker;
|
|
49
|
+
return label;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Present `options` through `select` without ever offering more than
|
|
53
|
+
* `SELECT_OPTION_LIMIT` entries in one dialog (AC6, PE-001, PE-012).
|
|
54
|
+
*
|
|
55
|
+
* - A list that fits — data plus `trailing` — opens exactly one dialog, in the
|
|
56
|
+
* caller's order with `trailing` last, so a ≤ cap flow is byte-identical to a
|
|
57
|
+
* plain `select` call.
|
|
58
|
+
* - A longer list pages `PAGED_SELECT_PAGE_SIZE` data entries at a time —
|
|
59
|
+
* `SELECT_OPTION_LIMIT - 3`, so the derived page never hardcodes the cap —
|
|
60
|
+
* appending `PAGED_SELECT_PREV` and/or `PAGED_SELECT_NEXT` before `trailing`.
|
|
61
|
+
* Pager entries navigate; `trailing` and data entries are returned to the
|
|
62
|
+
* caller; `undefined` (cancel) passes straight through.
|
|
63
|
+
*
|
|
64
|
+
* The two pager labels are reserved: a data value (or a `trailing` value) that
|
|
65
|
+
* spells one is indistinguishable from the pager entry in the string `select`
|
|
66
|
+
* answers with, so a colliding pager label is decorated until it names nothing
|
|
67
|
+
* else on the dialog — the data value stays selectable and so does the pager.
|
|
68
|
+
*/
|
|
69
|
+
export async function pagedSelect(select, title, options, { trailing } = {}) {
|
|
70
|
+
const items = [...options];
|
|
71
|
+
if (items.length + (trailing !== undefined ? 1 : 0) <= SELECT_OPTION_LIMIT) {
|
|
72
|
+
return await select(title, trailing !== undefined ? [...items, trailing] : items);
|
|
73
|
+
}
|
|
74
|
+
// A label-colliding data value must stay reachable, so compute the offered
|
|
75
|
+
// pager labels once against every string that can appear in a dialog.
|
|
76
|
+
const spoken = trailing !== undefined ? [...items, trailing] : items;
|
|
77
|
+
const prevLabel = pagerLabel(PAGED_SELECT_PREV, " ‹", spoken);
|
|
78
|
+
const nextLabel = pagerLabel(PAGED_SELECT_NEXT, " ›", spoken);
|
|
79
|
+
const pageCount = Math.ceil(items.length / PAGED_SELECT_PAGE_SIZE);
|
|
80
|
+
let page = 0;
|
|
81
|
+
for (;;) {
|
|
82
|
+
const start = page * PAGED_SELECT_PAGE_SIZE;
|
|
83
|
+
const entries = [
|
|
84
|
+
...items.slice(start, start + PAGED_SELECT_PAGE_SIZE),
|
|
85
|
+
...(page > 0 ? [prevLabel] : []),
|
|
86
|
+
...(page < pageCount - 1 ? [nextLabel] : []),
|
|
87
|
+
...(trailing !== undefined ? [trailing] : []),
|
|
88
|
+
];
|
|
89
|
+
const answer = await select(title, entries);
|
|
90
|
+
if (answer === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (answer === nextLabel && page < pageCount - 1) {
|
|
93
|
+
page += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (answer === prevLabel && page > 0) {
|
|
97
|
+
page -= 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
return answer;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The provider prefix Pi resolves a reference by — everything before the first
|
|
105
|
+
* `/` (SPEC Decision 6).
|
|
106
|
+
*
|
|
107
|
+
* Lenient on purpose, and deliberately distinct from the strict
|
|
108
|
+
* `parseModelReference` in `../config/schema.js`, which validates and rejects:
|
|
109
|
+
* this split must answer for any registry string. Both the console's
|
|
110
|
+
* provider-first grouping and the slash-aware filter below take their provider
|
|
111
|
+
* from here, so the two settings-side consumers cannot drift apart.
|
|
112
|
+
*/
|
|
113
|
+
export function providerOf(reference) {
|
|
114
|
+
const slash = reference.indexOf("/");
|
|
115
|
+
return slash === -1 ? reference : reference.slice(0, slash);
|
|
116
|
+
}
|
|
20
117
|
/**
|
|
21
118
|
* Token/subsequence, slash-aware reference filter (OB-2).
|
|
22
119
|
*
|
|
@@ -41,7 +138,7 @@ function matchesToken(token, ref) {
|
|
|
41
138
|
const provider = token.slice(0, -1);
|
|
42
139
|
if (provider === "")
|
|
43
140
|
return true;
|
|
44
|
-
const refProvider = ref
|
|
141
|
+
const refProvider = providerOf(ref);
|
|
45
142
|
return refProvider.startsWith(provider);
|
|
46
143
|
}
|
|
47
144
|
return isSubsequence(token, ref);
|
|
@@ -60,14 +157,27 @@ function isSubsequence(needle, haystack) {
|
|
|
60
157
|
* `filterReferences` as the operator types so the subsequence/slash-aware
|
|
61
158
|
* filter (OB-2) — not `SelectList`'s own prefix `setFilter` — drives what the
|
|
62
159
|
* window shows. Control keys (arrows, Enter, Escape) forward to the list.
|
|
160
|
+
*
|
|
161
|
+
* `initial` — the value in force — is selected in the window the picker opens
|
|
162
|
+
* with, through the SelectList's own `setSelectedIndex` (OB-3).
|
|
63
163
|
*/
|
|
64
164
|
export function createPickerComponent(options) {
|
|
65
|
-
const { items, maxVisible, theme, onSelect, onCancel } = options;
|
|
165
|
+
const { items, maxVisible, theme, initial, onSelect, onCancel } = options;
|
|
66
166
|
let filterText = "";
|
|
167
|
+
let openSelectionApplied = false;
|
|
67
168
|
const byValue = new Map(items.map((item) => [item.value, item]));
|
|
68
169
|
const buildList = () => {
|
|
69
170
|
const visible = filterReferences(filterText, items.map((item) => item.value)).map((value) => byValue.get(value) ?? { value, label: value });
|
|
70
171
|
const list = new SelectList(visible, maxVisible, theme);
|
|
172
|
+
// Preselection is an open-time concern (OB-3): the value in force applies to
|
|
173
|
+
// the first window only, so narrowing afterwards keeps the narrowed window's
|
|
174
|
+
// own first match instead of yanking the selection to a filtered-out value.
|
|
175
|
+
if (!openSelectionApplied) {
|
|
176
|
+
openSelectionApplied = true;
|
|
177
|
+
const index = initial === undefined ? -1 : visible.findIndex((item) => item.value === initial);
|
|
178
|
+
if (index > 0)
|
|
179
|
+
list.setSelectedIndex(index); // index 0 is already the SelectList default
|
|
180
|
+
}
|
|
71
181
|
list.onSelect = (item) => onSelect(item.value);
|
|
72
182
|
list.onCancel = onCancel;
|
|
73
183
|
return list;
|
package/package.json
CHANGED
|
@@ -1,62 +1,52 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workflow-status
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
|
|
6
6
|
license: MIT
|
|
7
7
|
argument-hint: "[--json-only] [--last-envelope <json|path>]"
|
|
8
8
|
description: >
|
|
9
|
-
Read-only workflow sensor:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
next", "state of the run".
|
|
9
|
+
Read-only workflow sensor: run the deterministic script, read the fixed
|
|
10
|
+
machine envelope, interpret the recommendation. Never edits. Triggers:
|
|
11
|
+
"workflow-status", "workflow status", "what can I build next".
|
|
13
12
|
---
|
|
14
13
|
|
|
15
14
|
# Workflow Status (the orchestrator's sensor)
|
|
16
15
|
|
|
17
|
-
One read-only pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
One read-only pass that answers, in a single fixed JSON envelope: **what exists,
|
|
17
|
+
what is blocked on what, what is startable right now, and what the recommended
|
|
18
|
+
next command is.** The **script is the deterministic producer**:
|
|
19
|
+
`scripts/workflow-status.mjs` executes the published `SENSOR_CORE` sequence
|
|
20
|
+
(steps 1–9 including 6a) and prints the envelope. This skill runs the script,
|
|
21
|
+
reads the JSON, interprets it against the references below, and prints the human
|
|
22
|
+
report — it never assembles the envelope by hand.
|
|
22
23
|
|
|
23
24
|
## Turn contract — verify before ending the turn
|
|
24
25
|
|
|
25
26
|
```
|
|
26
|
-
✓
|
|
27
|
-
|
|
27
|
+
✓ The script was RUN — `bun scripts/workflow-status.mjs [--json-only]
|
|
28
|
+
[--last-envelope <json|path>]` (node is the fallback when bun is absent, per
|
|
29
|
+
the repository's runtime convention); the envelope is the script's stdout, never
|
|
30
|
+
assembled by the model
|
|
28
31
|
✓ Nothing was edited, committed, pushed, or created — read-only, always
|
|
29
32
|
✓ `next.recommended` is non-bare (carries the unit's slug/NN, never a bare
|
|
30
|
-
`/plan-feature`) AND
|
|
31
|
-
current pre-execution evidence: `idea`/undesigned →
|
|
32
|
-
`defined` → `/plan-feature <slug>` only on a current
|
|
33
|
-
`/review-spec <slug>`; `planned`/`in-progress` → `/execute-phase <NN>` only on a
|
|
33
|
+
`/plan-feature`) AND the script computed it from the unit's resolved status
|
|
34
|
+
**and** its current pre-execution evidence: `idea`/undesigned →
|
|
35
|
+
`/design-feature <slug>`; `defined` → `/plan-feature <slug>` only on a current
|
|
36
|
+
`SPEC-REVIEW-PASS`, else `/review-spec <slug>`; `planned`/`in-progress` → `/execute-phase <NN>` only on a
|
|
34
37
|
current `PLAN-REVIEW-PASS`, else `/review-plan <NN>` (step 6a)
|
|
35
|
-
✓
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
✓ Every `detail.design_candidates[].next` begins with `/design-feature `
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`## Machine envelope` (never guessed)
|
|
48
|
-
✓ Per-unit `review`/`closure`/`issues_born` (steps 10–12) were computed per
|
|
49
|
-
their fixed rules — `adversarial.ran`/`n` stayed `null` unless real
|
|
50
|
-
evidence exists, never guessed — and any fired `next.suggested[]` entries
|
|
51
|
-
(step 13) quote their owning skill's condition verbatim, never a second
|
|
52
|
-
copy of the trigger logic
|
|
53
|
-
✓ The envelope is emitted on **every** invocation of this skill, including a
|
|
54
|
-
same-session natural-language follow-up about state — never replaced by prose
|
|
55
|
-
✓ The emitted envelope was checked against the shape reminders in
|
|
56
|
-
`## Machine envelope` (mirroring
|
|
57
|
-
`packages/agentic-workflow-schema/envelope.schema.json`) before printing
|
|
58
|
-
✓ The human-readable summary is printed, then the machine envelope (fenced
|
|
59
|
-
```json — see ## Machine envelope) is the ABSOLUTE last output
|
|
38
|
+
✓ `detail.crash_recovery` carries a verdict from the decision table and the
|
|
39
|
+
envelope `state` matches it (CLEAN→OK, RESUMABLE→CONTINUE,
|
|
40
|
+
AMBIGUOUS→NEEDS_INPUT)
|
|
41
|
+
✓ Every `detail.design_candidates[].next` begins with `/design-feature `
|
|
42
|
+
✓ Every degraded dimension is named in `detail.degradations` as
|
|
43
|
+
`unavailable-<source>-<cause>` — and, when `--last-envelope` was supplied, the
|
|
44
|
+
no-progress guard's `workflow_observations` note is present (never a silently
|
|
45
|
+
repeated bland recommendation)
|
|
46
|
+
✓ The envelope is emitted on **every** invocation, including a same-session
|
|
47
|
+
natural-language follow-up about state — never replaced by prose
|
|
48
|
+
✓ The human-readable summary is printed, then the machine envelope (the script's
|
|
49
|
+
JSON) is the ABSOLUTE last output
|
|
60
50
|
```
|
|
61
51
|
|
|
62
52
|
With `--json-only`, skip the human-readable summary: print the envelope alone.
|
|
@@ -66,43 +56,35 @@ With `--json-only`, skip the human-readable summary: print the envelope alone.
|
|
|
66
56
|
- Between orchestration steps: an external driver runs it to decide the next
|
|
67
57
|
command and model tier without parsing prose.
|
|
68
58
|
- Before picking work manually: "what can I start right now?"
|
|
69
|
-
- **Not** for judging quality (
|
|
70
|
-
|
|
59
|
+
- **Not** for judging quality (`review-change`/`audit-pr`) or product health
|
|
60
|
+
(`product-audit`) — this skill reports state, it never judges.
|
|
71
61
|
|
|
72
62
|
## Step 0 — Discover the project (always first)
|
|
73
63
|
|
|
74
|
-
Per the agent guide's **Workflow conventions** + **documentation map
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`progress.md` + `
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
Per the agent guide's **Workflow conventions** + **documentation map**. The
|
|
65
|
+
script reads what THIS skill needs (`docs/features/ROADMAP.md`, the fix index
|
|
66
|
+
`docs/fix/README.md`, every in-flight feature folder's `TASKS.md` +
|
|
67
|
+
`progress.md` + `review-findings.md`, and `docs/workflow/REPOSITORY_STATE.md`);
|
|
68
|
+
read them yourself only to interpret a field the script emitted.
|
|
80
69
|
|
|
81
70
|
## Progressive loading — fixed sensor route
|
|
82
71
|
|
|
83
72
|
The reference allowlist is exactly the seven linked paths below. Never invent or
|
|
84
|
-
read another `references/` path. This skill is a read-only sensor.
|
|
85
|
-
invocation loads this baseline in order:
|
|
73
|
+
read another `references/` path. This skill is a read-only sensor.
|
|
86
74
|
|
|
87
|
-
1. [sensor core](references/SENSOR_CORE.md)
|
|
75
|
+
1. [sensor core](references/SENSOR_CORE.md) — the sequence the script executes
|
|
88
76
|
2. [crash recovery](references/CRASH_RECOVERY.md)
|
|
89
77
|
3. [envelope core](references/ENVELOPE_CORE.md)
|
|
90
78
|
4. [envelope fields](references/ENVELOPE_FIELDS.md)
|
|
91
79
|
5. [pre-execution evidence](references/PRE_EXECUTION.md)
|
|
92
80
|
6. [guardrails](references/GUARDRAILS.md)
|
|
93
81
|
|
|
94
|
-
Add [pre-execution evidence](references/PRE_EXECUTION.md) whenever a unit is
|
|
95
|
-
`defined`, `planned` or `in-progress` — it defines step 6a (receipt sensing, the
|
|
96
|
-
one-label-per-stage table, and the legacy-adoption route).
|
|
97
82
|
Add [sensor signals](references/SENSOR_SIGNALS.md) only when a unit, issue,
|
|
98
83
|
finding, or recommendation exists; an empty project skips that file but still
|
|
99
84
|
emits the empty shapes defined by envelope fields. Add
|
|
100
85
|
[portability](references/PORTABILITY.md) only when the platform actually lacks a
|
|
101
86
|
named primitive. `--json-only` does not skip any baseline file.
|
|
102
87
|
|
|
103
|
-
All resources are one hop from this file. Missing required detail means
|
|
104
|
-
`BLOCKED`; do not fabricate a partial envelope.
|
|
105
|
-
|
|
106
88
|
## Portability
|
|
107
89
|
|
|
108
90
|
The sensor uses repository and forge commands only. When a named agent feature
|
|
@@ -122,28 +104,21 @@ the JSON contract.
|
|
|
122
104
|
|
|
123
105
|
## Done when
|
|
124
106
|
|
|
125
|
-
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
- The script ran and every claim in the report comes from its envelope — nothing
|
|
108
|
+
inferred from memory, nothing assembled by hand.
|
|
109
|
+
- `detail.design_candidates`, `detail.features`, `detail.fixes`,
|
|
110
|
+
`detail.startable_now`, `detail.blocked_units`, `detail.open_prs`,
|
|
111
|
+
`detail.untriaged_issues`, `detail.urgent`, `detail.degradations`, and
|
|
112
|
+
`detail.crash_recovery` were read from the envelope, and the envelope `state`
|
|
113
|
+
matches the crash-recovery verdict.
|
|
131
114
|
- With `--last-envelope` supplied: the no-progress guard ran — a stalled
|
|
132
115
|
`/plan-feature`/`/design-feature` hint surfaces as a `workflow_observations`
|
|
133
116
|
note, never a silent bland repeat, with no new write path introduced.
|
|
134
|
-
- The human summary (unless `--json-only`) and the envelope
|
|
135
|
-
|
|
136
|
-
blocked_units, open_prs, pending_triage, `untriaged_issues`
|
|
137
|
-
(count + oldest_open) and `urgent` (labels-only issue list +
|
|
138
|
-
interruptibility facts) — are printed, envelope last.
|
|
139
|
-
- Each `detail.features[]`/`detail.fixes[]` entry additionally carries
|
|
140
|
-
`review`, `closure`, and `issues_born` (steps 10–12) — `detail`-scoped, no
|
|
141
|
-
schema change — and any fired triggers appear in a top-level
|
|
142
|
-
`next.suggested[]` (step 13), single-sourced from the owning skill's own
|
|
143
|
-
condition text.
|
|
117
|
+
- The human summary (unless `--json-only`) and the envelope are printed, envelope
|
|
118
|
+
last.
|
|
144
119
|
- Nothing was modified anywhere.
|
|
145
120
|
|
|
146
121
|
→ Next: the envelope's `next.recommended` command — it is computed from the
|
|
147
122
|
actual state, so it IS the recommendation
|
|
148
123
|
· a human overview → read the printed table
|
|
149
|
-
· orchestrating programmatically → parse the
|
|
124
|
+
· orchestrating programmatically → parse the script's JSON
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
## Machine envelope
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`scripts/workflow-status.mjs` is the **deterministic producer** of this
|
|
4
|
+
envelope: it performs the assembly, runs the `validateEnvelope` self-check
|
|
5
|
+
(diagnostic only, exit 0), and prints the one JSON document. The skill runs the
|
|
6
|
+
script and interprets the JSON — it never assembles the envelope. Schema and
|
|
7
|
+
placement per the installed `orchestration-envelope` skill. The
|
|
4
8
|
`state` maps 1:1 from the crash-recovery verdict — **no new schema fields or
|
|
5
9
|
states** (the schema package needs no release):
|
|
6
10
|
|
|
@@ -68,6 +72,15 @@ these need **no package change**:
|
|
|
68
72
|
scope-bleed detection (widened by `#79`/`#89` to also match an issue
|
|
69
73
|
linked from an `## Amendments` row).
|
|
70
74
|
|
|
75
|
+
**Not yet mechanized (read this before consuming the three fields above).** The
|
|
76
|
+
`scripts/workflow-status.mjs` sensor does **not** emit `review`, `closure`, or
|
|
77
|
+
`issues_born` today: they are steps 10–12 of the published sequence and remain
|
|
78
|
+
skill-side work, so no `detail.features[]`/`detail.fixes[]` entry carries them
|
|
79
|
+
until a sensor phase owns them. A consumer must treat their absence as "not
|
|
80
|
+
computed", never as "empty": absence is not a `closure: absent-legacy` verdict
|
|
81
|
+
and not an `issues_born.n: 0`. Same discipline for `next.suggested[]` (step 13),
|
|
82
|
+
which is optional by contract and omitted when no trigger fired.
|
|
83
|
+
|
|
71
84
|
**`next.suggested[]`** — step 13's trigger-attributed suggestion surface,
|
|
72
85
|
`{command, trigger, source_skill}[]`, **optional** (mirrors
|
|
73
86
|
`packages/agentic-workflow-schema` 2.1.0's optional `EnvelopeSuggestion[]`).
|
|
@@ -77,7 +90,7 @@ second, drifting copy of that skill's logic. Advisory only: it rides beside
|
|
|
77
90
|
trigger this run → `next.suggested` is omitted entirely (an empty/absent
|
|
78
91
|
field, not an error).
|
|
79
92
|
|
|
80
|
-
**Envelope shape reminders (
|
|
93
|
+
**Envelope shape reminders (the script validates these before printing — mirrors
|
|
81
94
|
`packages/agentic-workflow-schema/envelope.schema.json`):**
|
|
82
95
|
|
|
83
96
|
- `blockers[].scope` ∈ `{"unit","run"}` — there is **no** `"code"` value;
|
|
@@ -101,6 +114,8 @@ field, not an error).
|
|
|
101
114
|
| `/triage-issue` | `strong` |
|
|
102
115
|
| `/product-audit` | `strong` |
|
|
103
116
|
| `/execute-phase` | `cheap` |
|
|
117
|
+
| `/review-spec` | `strong` |
|
|
118
|
+
| `/review-plan` | `strong` |
|
|
104
119
|
|
|
105
120
|
`next.tier` is read off this map by matching the resolved `next.recommended`
|
|
106
121
|
command's name (ignoring its arguments) — never guessed and never copied from
|
|
@@ -9,6 +9,15 @@ exact shape:
|
|
|
9
9
|
(swap `defined`/`/plan-feature` for `idea`/`/design-feature` on the design
|
|
10
10
|
side). The recommendation itself is unaffected — this only adds visibility.
|
|
11
11
|
|
|
12
|
+
**Shape vs. current output — read this before trusting the example below.** The
|
|
13
|
+
example is the envelope's **intended shape**, not everything the deterministic
|
|
14
|
+
producer emits today: per-unit `review`, `closure`, and `issues_born` (steps
|
|
15
|
+
10–12) and the `next.suggested[]` surface (step 13) are **not yet mechanized**
|
|
16
|
+
by `scripts/workflow-status.mjs` and do not appear in its output. A consumer
|
|
17
|
+
must read their absence as "not computed", never as `closure:
|
|
18
|
+
absent-legacy` or `issues_born.n: 0`; the owning note is
|
|
19
|
+
`ENVELOPE_CORE.md` §Not yet mechanized.
|
|
20
|
+
|
|
12
21
|
```json
|
|
13
22
|
{
|
|
14
23
|
"skill": "workflow-status",
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
## Process (fixed sequence —
|
|
1
|
+
## Process (fixed sequence — executed by the script)
|
|
2
|
+
|
|
3
|
+
`bun scripts/workflow-status.mjs` (node when bun is absent — the repository's
|
|
4
|
+
runtime convention) executes this whole sequence (git/forge
|
|
5
|
+
collection, urgency labels-only scan, roadmap + fix-index parse, dependency
|
|
6
|
+
closure, readiness + step-6a receipt sensing, phase progress, review-mark
|
|
7
|
+
currency, fix-now fold projection) and prints the fixed Envelope v2 JSON. The
|
|
8
|
+
numbered blocks below state the semantics the script implements; the model reads
|
|
9
|
+
the JSON and interprets it.
|
|
2
10
|
|
|
3
11
|
### Normalized Repository State
|
|
4
12
|
|
|
@@ -12,14 +20,6 @@ readiness classification, if the ledger is missing, `draft`, `contradicted`, or
|
|
|
12
20
|
sensor remains read-only; it never edits or resolves the ledger. Conflicting
|
|
13
21
|
live evidence against a frozen ledger remains a contradiction candidate.
|
|
14
22
|
|
|
15
|
-
1. **Git state.** `git branch --show-current`, `git status --porcelain`,
|
|
16
|
-
`git fetch` + `git status -sb`. A dirty tree or unpushed branch is reported
|
|
17
|
-
as-is (a `workflow`-kind observation in `detail`), never cleaned up.
|
|
18
|
-
2. **Forge state.** List open + recently merged PRs and open issues with the
|
|
19
|
-
declared forge CLI (examples use `gh`):
|
|
20
|
-
`gh pr list --state open --json number,title,headRefName,url,statusCheckRollup`,
|
|
21
|
-
`gh pr list --state merged --limit 20 --json number,headRefName`,
|
|
22
|
-
`gh issue list --state open --json number,title,labels`.
|
|
23
23
|
3. **Urgency labels (`detail.urgent`) — labels-only, presence-only, never
|
|
24
24
|
decides.** Reuse the open-issue list from step 2 (`gh issue list --json
|
|
25
25
|
labels` — the JSON labels array already carried by that call); no separate
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
persisted marker, see `#76`") — never infer `true`/`false` from
|
|
29
29
|
absence of evidence.
|
|
30
30
|
11. **Per-unit closure state (`detail.*.closure`).** `{state}` ∈
|
|
31
|
-
`present | absent-legacy | blocked` — reuse `audit-pr`'s own
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
`present | absent-legacy | blocked` — reuse `audit-pr`'s own check
|
|
32
|
+
verbatim (`skills/audit-pr/SKILL.md`, "Closure integrity"): grep the SPEC
|
|
33
|
+
for a `Capability closure` heading.
|
|
34
34
|
Fix-governed unit → `n/a` (fix SPECs carry no closure block by design,
|
|
35
35
|
same carve-out `audit-pr` applies). Feature SPEC, block absent →
|
|
36
36
|
`absent-legacy`. Feature SPEC, block present with any blank row or a
|
|
37
37
|
resolved non-`n/a` row unmapped to an acceptance criterion → `blocked`.
|
|
38
38
|
Feature SPEC, block present and every row filled or `n/a`-justified and
|
|
39
|
-
mapped → `present`. Single-sourced: never re-derive
|
|
40
|
-
|
|
39
|
+
mapped → `present`. Single-sourced: never re-derive it — re-run
|
|
40
|
+
`audit-pr`'s grep.
|
|
41
41
|
12. **Per-unit descope provenance (`detail.*.issues_born`).**
|
|
42
42
|
`{n, with_descope_amendment}` — reuse `audit-pr`'s scope-bleed gate
|
|
43
43
|
detection verbatim (`skills/audit-pr/SKILL.md` "Scope integrity
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
slug/number text match `audit-pr` itself defines — **never** an issue's
|
|
51
51
|
free-text body beyond that defined match (injection-safety, mirrors
|
|
52
52
|
`detail.urgent`'s labels-only discipline).
|
|
53
|
+
|
|
54
|
+
*Steps 10–12's fields are **not yet emitted** — see `ENVELOPE_CORE.md` §Not yet
|
|
55
|
+
mechanized.*
|
|
53
56
|
13. **`next.suggested[]` — single-sourced trigger surface.** One entry per
|
|
54
57
|
**fired** trigger the driver can act on now, `{command, trigger,
|
|
55
58
|
source_skill}` — the `trigger` string **quotes**, never paraphrases, the
|