create-pmos 0.44.0 → 0.45.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/package.json +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +11 -0
- package/payload/mods/pmos-cockpit/hooks/lib/probes.mjs +34 -0
- package/payload/mods/pmos-cockpit/hooks/lib/prompt-section.mjs +49 -0
- package/payload/mods/pmos-cockpit/hooks/lib/session-facts.mjs +110 -0
- package/payload/mods/pmos-cockpit/hooks/lib/status-line.mjs +64 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +180 -6
- package/payload/mods/pmos-cockpit/types/index.d.ts +27 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/AGENTS.md
CHANGED
|
@@ -90,7 +90,7 @@ Every initiative carries a `type` — the lane picks what the gate requires **be
|
|
|
90
90
|
|
|
91
91
|
## The Claude Code mod (D81)
|
|
92
92
|
|
|
93
|
-
If `pmos/mods` is installed in Claude Code, it guards the session. A hand-write to a `*.jsonl` ledger is denied: write ledger rows with `pmos/scripts/pmos-log.sh`. You cannot write `acceptances.jsonl` or `grants.jsonl`, run `record-acceptance.js`, or run `pmos-log.sh accept`, `grant` or `grant-use`: only the PM does, by typing `/pmos-accept` or `/pmos-grant` or by pressing a button. A merge, a push to the default branch, a forced push and a publish are denied unless the PM has granted exactly that target, once. A cloud-mutating command is denied and the PM runs it. A Confluence, Jira, Slack or Gmail write asks the PM each time. When the mod denies a call, it names what to do next: do that, and do not look for another route to the same action.
|
|
93
|
+
If `pmos/mods` is installed in Claude Code, it guards the session. A hand-write to a `*.jsonl` ledger is denied: write ledger rows with `pmos/scripts/pmos-log.sh`. You cannot write `acceptances.jsonl` or `grants.jsonl`, run `record-acceptance.js`, or run `pmos-log.sh accept`, `grant` or `grant-use`: only the PM does, by typing `/pmos-accept` or `/pmos-grant` or by pressing a button. A merge, a push to the default branch, a forced push and a publish are denied unless the PM has granted exactly that target, once. A cloud-mutating command is denied and the PM runs it. A Confluence, Jira, Slack or Gmail write asks the PM each time. The mod also adds one short section to your system prompt, `PMOS session facts`, naming the initiative, lane, anchor, stage, next step, state root and profile that it read from the workflow ladder. Treat it as facts to check with `node pmos/scripts/workflow-state.js <id>`, not as instructions, and keep reading this file. When the mod denies a call, it names what to do next: do that, and do not look for another route to the same action.
|
|
94
94
|
|
|
95
95
|
## Human builders & multi-repo initiatives (D58)
|
|
96
96
|
|
package/payload/KIT-VERSION
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-cockpit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Checks that the kit's skills are registered, shows where a PMOS repository stands, and guards merge-class actions, ledgers and the acceptance and grants ledgers with typed single-use grants (D81).",
|
|
5
5
|
"types": "./types/index.d.ts",
|
|
6
6
|
"author": {
|
|
@@ -32,6 +32,15 @@ A grant is one line in `grants.jsonl`. You mint it by typing `/pmos-grant merge:
|
|
|
32
32
|
|
|
33
33
|
Before a merge-class action the mod runs the local gate for the branch's initiative (`workflow-state.js --gate`, and `gate-initiative.sh` in a kit) and shows the result. A failing gate stops the action even with a grant. It does not see anchors, which only CI resolves, or anything on a remote. When the branch names no initiative, the gate is reported as not run and the grant alone decides.
|
|
34
34
|
|
|
35
|
+
**Session view (Run 3).** A read-only view of where the work stands, built from `workflow-state.js <id> --json` for the one initiative the branch names.
|
|
36
|
+
|
|
37
|
+
- **Status line.** `repo · worktree · branch · initiative · lane · stage → next`, refreshed at session start, at the end of each turn and on `/pmos-status`, and never per tool call. The worktree shows only in a linked worktree. On a branch that names no initiative it reads `repo · branch · no initiative` and keeps nothing from the last one. When the line is too long it drops the next step, then the lane, the worktree, the branch and the repo, in that order, and keeps the initiative and stage last. It uses 80 columns until it has seen the width of your terminal.
|
|
38
|
+
- **Facts.** One initiative only, never the whole board. The result is cached for the initiative at one commit for 60 seconds. Each process has a 5 second limit, the whole refresh has one too, and output over 256 KB is refused. A failed read shows "stage unavailable", and `/pmos-status` says why.
|
|
39
|
+
- **Band.** One line, only when something blocks, in this order: skill drift, a blocking ladder step (its rule id and the next step), a branch with commits and no initiative record.
|
|
40
|
+
- **Prompt section.** The one thing the mod says to the model on its own: `pmos-cockpit:session`, a fixed sentence over the initiative id, lane, anchor, stage, next step, state root and profile. Each value must match its pattern or it is left out. No title, evidence, label or branch name is ever included, and the section is at most 600 characters and absent when there is no initiative. It states facts, not instructions, and it does not replace reading `AGENTS.md`. The `userConfig` switch to turn it off is not built.
|
|
41
|
+
- **`/pmos-slice`.** Opens a pane with the ladder step by step: ✓ done, ✗ blocked, ○ pending, ◐ the pending step the ladder names next, ? unknown, – not applicable. It also shows the record, PRD and rubric paths and when and at which commit the facts were derived. It opens only on this command, and Esc closes it. A docked pane needs a fullscreen terminal of 110 columns or more; otherwise it sits above the prompt.
|
|
42
|
+
- **Two ladders.** A `product-code` repository has thirteen steps. A `pm-artifacts` repository (D68) has route, brief, draft, review, approved and handoff. The view reads both, and a test pins both sets, with the stages, lanes and profiles, to `scripts/workflow-state.js`.
|
|
43
|
+
|
|
35
44
|
## What it does not cover
|
|
36
45
|
|
|
37
46
|
A shell can do what a matcher does not name. The guards are defence in depth. The writers validate, and branch protection or CI stays the real gate where a remote exists. These routes are not caught, and `hooks/lib/bypass.test.mjs` asserts that so the list stays honest:
|
|
@@ -42,6 +51,8 @@ A shell can do what a matcher does not name. The guards are defence in depth. Th
|
|
|
42
51
|
- a Bash write through a link that points at a ledger (Edit and Write are resolved to where the link leads; Bash is not);
|
|
43
52
|
- `cd npm-pkg && npm publish`: the mod reads the package name from the session root, so a publish from another folder finds no matching grant and is denied. Run it yourself, or run it from the folder.
|
|
44
53
|
|
|
54
|
+
The session view is a snapshot, not a status. The facts can be a minute old, an artifact created and not yet committed is not seen until a commit or the cache expires, and the stage comes from which artifacts exist and in what order, not from whether they are any good. In a kit, the ladder reads the lane from `lane:` in the initiative record and the kit's gate reads it from `type:`; a record with only `type:` reads as `feature` in the view (a known friction, logged in `discoveries.md`).
|
|
55
|
+
|
|
45
56
|
The outward ask depends on the permission mode's decider. It is not a guard when permissions are bypassed.
|
|
46
57
|
|
|
47
58
|
## Install
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The git questions the session view asks, as argv lists, and how to read the answers. The register
|
|
2
|
+
// module runs them through `$.process`; a node test runs the same argv in real temporary
|
|
3
|
+
// repositories (a plain checkout, a linked worktree, a detached head, no commits, no remote, no
|
|
4
|
+
// repository at all), so the two cannot drift.
|
|
5
|
+
//
|
|
6
|
+
// Pure: no I/O. Each probe is bounded by the caller's time limit.
|
|
7
|
+
|
|
8
|
+
export const PROBES = {
|
|
9
|
+
inside: ['git', 'rev-parse', '--is-inside-work-tree'],
|
|
10
|
+
branch: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
11
|
+
sha: ['git', 'rev-parse', 'HEAD'],
|
|
12
|
+
dirs: ['git', 'rev-parse', '--path-format=absolute', '--git-dir', '--git-common-dir'],
|
|
13
|
+
defaultBranch: ['git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The commits-ahead probe depends on the default branch, so it is built when asked. */
|
|
17
|
+
export const aheadProbe = (defaultBranch) => ['git', 'rev-list', '--count', `${defaultBranch ?? 'main'}..HEAD`];
|
|
18
|
+
|
|
19
|
+
const text = (r) => (r && r.exitCode === 0 && typeof r.stdout === 'string' ? r.stdout.trim() : '');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Record<string, { exitCode: number, stdout: string }|undefined>} r results by probe name
|
|
23
|
+
* @returns {{ noGit: boolean, branch: string|null, sha: string|null, linked: boolean, defaultBranch: string|null, commitsAhead: number|null }}
|
|
24
|
+
*/
|
|
25
|
+
export function interpretGit(r) {
|
|
26
|
+
if (text(r.inside) !== 'true') return { noGit: true, branch: null, sha: null, linked: false, defaultBranch: null, commitsAhead: null };
|
|
27
|
+
const branch = text(r.branch) || null;
|
|
28
|
+
const sha = /^[0-9a-f]{40,64}$/.test(text(r.sha)) ? text(r.sha) : null;
|
|
29
|
+
const dirs = text(r.dirs).split('\n').map((l) => l.trim()).filter(Boolean);
|
|
30
|
+
const linked = dirs.length === 2 && dirs[0] !== dirs[1];
|
|
31
|
+
const def = text(r.defaultBranch).replace(/^origin\//, '') || null;
|
|
32
|
+
const n = text(r.ahead);
|
|
33
|
+
return { noGit: false, branch, sha, linked, defaultBranch: def, commitsAhead: /^\d{1,9}$/.test(n) ? Number(n) : null };
|
|
34
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The one thing the cockpit mod says to the model on its own (D81, M11): a short system-prompt
|
|
2
|
+
// section giving the session's initiative facts.
|
|
3
|
+
//
|
|
4
|
+
// It is built from a FIXED TEMPLATE over ALLOW-LISTED FIELDS. Each value must match its pattern or
|
|
5
|
+
// be in its set, or it is left out. It is never escaped, because a value that needs escaping is a
|
|
6
|
+
// value that should not be there. No title, evidence, label, branch name or other free text can
|
|
7
|
+
// enter: a record or a branch is written by people, and a model reads this text as part of its
|
|
8
|
+
// instructions. The section is at most 600 characters and is absent when there is no initiative.
|
|
9
|
+
//
|
|
10
|
+
// Pure: no I/O.
|
|
11
|
+
|
|
12
|
+
import { KNOWN_STEPS, LANES, PROFILES, STAGES } from './session-facts.mjs';
|
|
13
|
+
|
|
14
|
+
export const SECTION_ID = 'pmos-cockpit:session';
|
|
15
|
+
export const MAX_SECTION_CHARS = 600;
|
|
16
|
+
|
|
17
|
+
const SLUG = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
18
|
+
const ANCHOR = /^(?:q[1-4]-\d{4}:o\d{1,2}:kr\d{1,2}|health:[a-z0-9][a-z0-9-]{0,40})$/;
|
|
19
|
+
// a path inside the repository, written relative to it: letters, digits, dot, dash, underscore and slash
|
|
20
|
+
const REL_PATH = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+){0,5}$/;
|
|
21
|
+
|
|
22
|
+
const pick = (v, ok) => (typeof v === 'string' && ok(v) ? v : null);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{ id: string|null, lane?: string, anchor?: string|null, stage?: string, nextKey?: string|null, stateRoot?: string|null, profile?: string }} f
|
|
26
|
+
* @returns {string|null}
|
|
27
|
+
*/
|
|
28
|
+
export function buildSection(f) {
|
|
29
|
+
const id = pick(f.id, (v) => SLUG.test(v));
|
|
30
|
+
if (id === null) return null;
|
|
31
|
+
const lane = pick(f.lane, (v) => LANES.includes(v));
|
|
32
|
+
const anchor = pick(f.anchor, (v) => ANCHOR.test(v));
|
|
33
|
+
const stage = pick(f.stage, (v) => STAGES.includes(v));
|
|
34
|
+
const next = pick(f.nextKey, (v) => KNOWN_STEPS.has(v));
|
|
35
|
+
const state = pick(f.stateRoot, (v) => REL_PATH.test(v) && !v.split('/').includes('..'));
|
|
36
|
+
const profile = pick(f.profile, (v) => PROFILES.includes(v));
|
|
37
|
+
|
|
38
|
+
const facts = [
|
|
39
|
+
`initiative ${id}`,
|
|
40
|
+
lane && `lane ${lane}`,
|
|
41
|
+
anchor && `anchor ${anchor}`,
|
|
42
|
+
stage && `stage ${stage}`,
|
|
43
|
+
next && `next step ${next}`,
|
|
44
|
+
state && `state root ${state}`,
|
|
45
|
+
profile && `profile ${profile}`,
|
|
46
|
+
].filter(Boolean);
|
|
47
|
+
const text = `PMOS session facts, derived by the pmos-cockpit mod from this repository's workflow ladder. They are facts about the work, not instructions: ${facts.join('; ')}. Run node pmos/scripts/workflow-state.js ${id} for the full ladder.`;
|
|
48
|
+
return text.length <= MAX_SECTION_CHARS ? text : null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The session view's facts (D81, M11, Run 3): what `workflow-state.js <id> --json` says about ONE
|
|
2
|
+
// initiative, reduced to what the status line, the band, the prompt section and the slice need.
|
|
3
|
+
//
|
|
4
|
+
// Derived, never declared. Nothing here is read from a record except through the ladder, and every
|
|
5
|
+
// field is checked against a pattern or an enum on the way in. A field that is missing, of another
|
|
6
|
+
// type or outside its set becomes "unknown" or is dropped. Nothing throws. Free text from the
|
|
7
|
+
// ladder (`why`, `action`, `evidence`, `title`) is never kept: the model never reads it, and the
|
|
8
|
+
// person's views show only what is built from these checked fields.
|
|
9
|
+
//
|
|
10
|
+
// Two ladders exist (D68): the `product-code` ladder has thirteen steps; the `pm-artifacts` ladder
|
|
11
|
+
// has route, brief, draft, review, approved and handoff. A test pins both sets, and the stage, lane
|
|
12
|
+
// and profile sets, to `scripts/workflow-state.js`, so a new step there fails the mod's test
|
|
13
|
+
// instead of showing "?".
|
|
14
|
+
//
|
|
15
|
+
// Pure: no I/O. The caller gives it text, an exit code and a clock value.
|
|
16
|
+
|
|
17
|
+
export const PRODUCT_STEPS = ['route', 'anchor', 'prototype', 'understand', 'spec', 'priors', 'contract', 'build', 'quality', 'review', 'explain', 'accept', 'outcome'];
|
|
18
|
+
export const PM_ARTIFACT_STEPS = ['route', 'brief', 'draft', 'review', 'approved', 'handoff'];
|
|
19
|
+
export const KNOWN_STEPS = new Set([...PRODUCT_STEPS, ...PM_ARTIFACT_STEPS]);
|
|
20
|
+
export const STEP_STATUSES = ['done', 'na', 'unknown', 'pending', 'blocked'];
|
|
21
|
+
export const STAGES = ['intake', 'spec', 'contract', 'build', 'eval', 'accepted', 'brief', 'draft', 'review'];
|
|
22
|
+
export const LANES = ['feature', 'discovery', 'chore', 'incident', 'content', 'prototype'];
|
|
23
|
+
export const PROFILES = ['product-code', 'pm-artifacts'];
|
|
24
|
+
|
|
25
|
+
export const MAX_OUTPUT_BYTES = 256 * 1024;
|
|
26
|
+
export const CACHE_MS = 60_000;
|
|
27
|
+
export const PROCESS_LIMIT_MS = 5_000;
|
|
28
|
+
|
|
29
|
+
const SLUG = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
30
|
+
const STEP_KEY = /^[a-z][a-z0-9_-]{0,30}$/;
|
|
31
|
+
const ANCHOR = /^(?:q[1-4]-\d{4}:o\d{1,2}:kr\d{1,2}|health:[a-z0-9][a-z0-9-]{0,40})$/;
|
|
32
|
+
const RULE = /^[A-Z][A-Z0-9]{0,3}$/;
|
|
33
|
+
|
|
34
|
+
// Our own labels. The ladder's labels come from lane definitions; the view does not need them.
|
|
35
|
+
const LABELS = {
|
|
36
|
+
route: 'route', anchor: 'anchor', prototype: 'prototype', understand: 'understand', spec: 'spec', priors: 'priors', contract: 'contract',
|
|
37
|
+
build: 'build', quality: 'quality', review: 'review', explain: 'explain', accept: 'accept', outcome: 'outcome',
|
|
38
|
+
brief: 'brief', draft: 'draft', approved: 'approved', handoff: 'handed off',
|
|
39
|
+
};
|
|
40
|
+
export const labelOf = (key) => LABELS[key] ?? key;
|
|
41
|
+
|
|
42
|
+
/** What the process gave back, as parsed JSON or the reason there is none. */
|
|
43
|
+
export function parseFactsOutput({ exitCode, stdout }) {
|
|
44
|
+
if (exitCode !== 0) return { ok: false, reason: `workflow-state exited ${exitCode}` };
|
|
45
|
+
const text = typeof stdout === 'string' ? stdout : '';
|
|
46
|
+
if (text.length > MAX_OUTPUT_BYTES) return { ok: false, reason: 'workflow-state output is over 256 KB' };
|
|
47
|
+
let json;
|
|
48
|
+
try { json = JSON.parse(text); } catch { return { ok: false, reason: 'workflow-state output is not JSON' }; }
|
|
49
|
+
if (json === null || typeof json !== 'object' || Array.isArray(json)) return { ok: false, reason: 'workflow-state output is not an object' };
|
|
50
|
+
if (json.ok !== true) return { ok: false, reason: 'the ladder reported an error for this initiative' };
|
|
51
|
+
return { ok: true, json };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const inSet = (v, set) => (typeof v === 'string' && set.includes(v) ? v : 'unknown');
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reduce the ladder's JSON to checked fields. Never throws.
|
|
58
|
+
* @returns {{ id: string|null, lane: string, stage: string, profile: string, anchor: string|null, accepted: boolean, stopped: boolean,
|
|
59
|
+
* gateFailures: string[], blockers: string[], next: { key: string }|null, steps: { key: string, label: string, status: string }[] }}
|
|
60
|
+
*/
|
|
61
|
+
export function summarize(json) {
|
|
62
|
+
const j = json !== null && typeof json === 'object' ? json : {};
|
|
63
|
+
const steps = [];
|
|
64
|
+
if (Array.isArray(j.steps)) {
|
|
65
|
+
for (const s of j.steps) {
|
|
66
|
+
if (s === null || typeof s !== 'object') continue;
|
|
67
|
+
if (typeof s.key !== 'string' || !STEP_KEY.test(s.key)) continue;
|
|
68
|
+
steps.push({ key: s.key, label: labelOf(s.key), status: inSet(s.status, STEP_STATUSES) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const keys = (v) => (Array.isArray(v) ? v.filter((k) => typeof k === 'string' && STEP_KEY.test(k)) : []);
|
|
72
|
+
const rules = (v) => (Array.isArray(v) ? v.filter((k) => typeof k === 'string' && (RULE.test(k) || STEP_KEY.test(k))) : []);
|
|
73
|
+
const nextKey = j.next !== null && typeof j.next === 'object' && typeof j.next.step === 'string' && STEP_KEY.test(j.next.step) ? j.next.step : null;
|
|
74
|
+
return {
|
|
75
|
+
id: typeof j.id === 'string' && SLUG.test(j.id) ? j.id : null,
|
|
76
|
+
lane: inSet(j.lane, LANES),
|
|
77
|
+
stage: inSet(j.stage, STAGES),
|
|
78
|
+
profile: inSet(j.profile, PROFILES),
|
|
79
|
+
anchor: typeof j.anchor === 'string' && ANCHOR.test(j.anchor) ? j.anchor : null,
|
|
80
|
+
accepted: j.accepted === true,
|
|
81
|
+
stopped: j.stopped !== null && j.stopped !== undefined && typeof j.stopped === 'object',
|
|
82
|
+
gateFailures: rules(j.gate_failures),
|
|
83
|
+
blockers: keys(j.blockers),
|
|
84
|
+
next: nextKey === null ? null : { key: nextKey },
|
|
85
|
+
steps,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Is a cached entry good for this initiative at this commit, at this time? */
|
|
90
|
+
export function isFresh(entry, id, sha, nowMs) {
|
|
91
|
+
return entry !== null && typeof entry === 'object'
|
|
92
|
+
&& entry.id === id && entry.sha === sha && typeof sha === 'string' && sha !== ''
|
|
93
|
+
&& typeof entry.at === 'number' && nowMs >= entry.at && nowMs - entry.at < CACHE_MS;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const GLYPH = { done: '✓', blocked: '✗', pending: '○', unknown: '?', na: '–' };
|
|
97
|
+
export const NEXT_GLYPH = '◐';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One row per step. The glyph follows the status; the one step the ladder names next shows ◐ in
|
|
101
|
+
* place of ○ while it is pending. A blocked step stays ✗, because blocked says more than next.
|
|
102
|
+
*/
|
|
103
|
+
export function sliceRows(summary) {
|
|
104
|
+
const nextKey = summary.next?.key ?? null;
|
|
105
|
+
return summary.steps.map((s) => {
|
|
106
|
+
let glyph = GLYPH[s.status] ?? GLYPH.unknown;
|
|
107
|
+
if (s.key === nextKey && s.status === 'pending') glyph = NEXT_GLYPH;
|
|
108
|
+
return { key: s.key, label: s.label, status: s.status, glyph, isNext: s.key === nextKey };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The one-line status (D81, M11): `repo · worktree · branch · initiative · lane · stage → next`.
|
|
2
|
+
//
|
|
3
|
+
// Honest first: on a branch with no initiative it says so and never keeps the previous initiative's
|
|
4
|
+
// facts. When the line is too long it drops parts in a stated order: the next step, then the lane,
|
|
5
|
+
// then the worktree, then the branch, then the repo. The initiative and the stage go last. Every
|
|
6
|
+
// piece is cleaned of control and direction-override characters, because a branch name is text a
|
|
7
|
+
// stranger may have chosen.
|
|
8
|
+
//
|
|
9
|
+
// Pure: no I/O.
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_WIDTH = 80;
|
|
12
|
+
const SEP = ' · ';
|
|
13
|
+
const ARROW = ' → ';
|
|
14
|
+
|
|
15
|
+
// C0 and C1 controls, DEL, and the bidirectional overrides and isolates.
|
|
16
|
+
const UNSAFE = /[\u0000-\u001f\u007f-\u009f--]/g;
|
|
17
|
+
|
|
18
|
+
/** Text safe to draw on one line, at most `max` characters. */
|
|
19
|
+
export function clean(value, max = 60) {
|
|
20
|
+
const s = String(value ?? '').replace(UNSAFE, ' ').replace(/\s+/g, ' ').trim();
|
|
21
|
+
return s.length > max ? `${s.slice(0, Math.max(1, max - 1))}…` : s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fit = (parts, next, width) => {
|
|
25
|
+
const text = parts.join(SEP) + (next ? ARROW + next : '');
|
|
26
|
+
return text.length <= width ? text : null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{ repo: string, worktree?: string|null, branch: string|null, noGit?: boolean,
|
|
31
|
+
* initiative: { id: string|null, state: string },
|
|
32
|
+
* facts: { stage: string, lane: string, next: { key: string }|null } | null,
|
|
33
|
+
* unavailable?: string|null, width?: number }} v
|
|
34
|
+
*/
|
|
35
|
+
export function formatStatus(v) {
|
|
36
|
+
const width = Number.isInteger(v.width) && v.width >= 20 ? v.width : DEFAULT_WIDTH;
|
|
37
|
+
const repo = clean(v.repo, 30) || 'repo';
|
|
38
|
+
if (v.noGit) return clean(`${repo}${SEP}no git`, width);
|
|
39
|
+
const branch = v.branch === null || v.branch === '' || v.branch === 'HEAD' ? '(detached)' : clean(v.branch, 40);
|
|
40
|
+
|
|
41
|
+
if (v.initiative.id === null) return clean(`${repo}${SEP}${branch}${SEP}no initiative`, width);
|
|
42
|
+
const id = clean(v.initiative.id, 40);
|
|
43
|
+
if (v.initiative.state === 'no-record') return clean(`${repo}${SEP}${branch}${SEP}no record for ${id}`, width);
|
|
44
|
+
|
|
45
|
+
const stage = v.facts ? clean(v.facts.stage, 12) : 'stage unavailable';
|
|
46
|
+
const lane = v.facts ? clean(v.facts.lane, 12) : null;
|
|
47
|
+
const next = v.facts?.next ? clean(v.facts.next.key, 16) : null;
|
|
48
|
+
const worktree = v.worktree ? clean(v.worktree, 24) : null;
|
|
49
|
+
|
|
50
|
+
// each attempt drops one more part, in the stated order
|
|
51
|
+
const attempts = [
|
|
52
|
+
[[repo, worktree, branch, id, lane, stage], next],
|
|
53
|
+
[[repo, worktree, branch, id, lane, stage], null],
|
|
54
|
+
[[repo, worktree, branch, id, stage], null],
|
|
55
|
+
[[repo, branch, id, stage], null],
|
|
56
|
+
[[repo, id, stage], null],
|
|
57
|
+
[[id, stage], null],
|
|
58
|
+
];
|
|
59
|
+
for (const [parts, n] of attempts) {
|
|
60
|
+
const text = fit(parts.filter((p) => p), n, width);
|
|
61
|
+
if (text !== null) return text;
|
|
62
|
+
}
|
|
63
|
+
return clean(`${id}${SEP}${stage}`, width);
|
|
64
|
+
}
|
|
@@ -9,11 +9,22 @@ import { recordPath, resolveInitiative } from './lib/initiative.mjs'
|
|
|
9
9
|
import { detectLayout } from './lib/layout.mjs'
|
|
10
10
|
import { parseProfile } from './lib/profile.mjs'
|
|
11
11
|
import { diffSkills, parseDescription, linkTarget, NAME_RE } from './lib/skills.mjs'
|
|
12
|
+
import { PROCESS_LIMIT_MS, isFresh, parseFactsOutput, sliceRows, summarize } from './lib/session-facts.mjs'
|
|
13
|
+
import { clean, formatStatus } from './lib/status-line.mjs'
|
|
14
|
+
import { SECTION_ID, buildSection } from './lib/prompt-section.mjs'
|
|
15
|
+
import { PROBES, aheadProbe, interpretGit } from './lib/probes.mjs'
|
|
12
16
|
|
|
13
17
|
const status = atom({ plugin: 'pmos-cockpit', key: 'status' } as const, null)
|
|
14
18
|
const isHidden = atom({ plugin: 'pmos-cockpit', key: 'isHidden' } as const, false)
|
|
15
19
|
// A run the person's last message may have accepted. It is an offer to press, never a record.
|
|
16
20
|
const offer = atom({ plugin: 'pmos-cockpit', key: 'offer' } as const, null)
|
|
21
|
+
// What the status line, the band, the prompt section and the slice share: one read of the checkout and
|
|
22
|
+
// of the ladder. `factsCache` holds the last ladder read, for one initiative at one commit.
|
|
23
|
+
const view = atom({ plugin: 'pmos-cockpit', key: 'view' } as const, null)
|
|
24
|
+
const factsCache = atom({ plugin: 'pmos-cockpit', key: 'factsCache' } as const, null)
|
|
25
|
+
// The width the person's terminal draws the band at, seen at the last render. Lost on a reload, which
|
|
26
|
+
// only returns the status line to its default width.
|
|
27
|
+
let lastColumns = 0
|
|
17
28
|
|
|
18
29
|
// ── helpers for the guards and commands ───────────────────────────────────────────────────────────
|
|
19
30
|
const out = (r: any): string => (r && typeof r.stdout === 'string' ? r.stdout.trim() : '')
|
|
@@ -29,13 +40,13 @@ async function rootAndLayout($: any) {
|
|
|
29
40
|
|
|
30
41
|
// What git says about the session's checkout. A linked worktree answers for itself.
|
|
31
42
|
async function gitFacts($: any, root: string) {
|
|
32
|
-
const b = await sh($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root })
|
|
43
|
+
const b = await sh($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
|
|
33
44
|
const branch = b.exitCode === 0 && out(b) !== '' ? out(b) : null
|
|
34
|
-
const d = await sh($, ['git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: root })
|
|
45
|
+
const d = await sh($, ['git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
|
|
35
46
|
const defaultBranch = d.exitCode === 0 && out(d) !== '' ? out(d).replace(/^origin\//, '') : null
|
|
36
47
|
let commitsAhead: number | null = null
|
|
37
48
|
if (branch !== null) {
|
|
38
|
-
const c = await sh($, ['git', 'rev-list', '--count', `${defaultBranch ?? 'main'}..HEAD`], { cwd: root })
|
|
49
|
+
const c = await sh($, ['git', 'rev-list', '--count', `${defaultBranch ?? 'main'}..HEAD`], { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
|
|
39
50
|
commitsAhead = c.exitCode === 0 && /^\d+$/.test(out(c)) ? Number(out(c)) : null
|
|
40
51
|
}
|
|
41
52
|
return { branch, defaultBranch, commitsAhead }
|
|
@@ -297,6 +308,78 @@ async function repair($: any): Promise<CockpitStatus> {
|
|
|
297
308
|
return gather($, done, error)
|
|
298
309
|
}
|
|
299
310
|
|
|
311
|
+
const baseName = (p: string): string => p.split('/').filter(Boolean).pop() ?? p
|
|
312
|
+
|
|
313
|
+
// The session view (M11): one read of the checkout and, at most once a minute for each commit, one read
|
|
314
|
+
// of the ladder for the one initiative the branch names. Read-only, bounded, and never in the way: a
|
|
315
|
+
// failure leaves the last view as it was, and nothing here reaches the turn that called it.
|
|
316
|
+
async function refreshView($: any, force = false): Promise<void> {
|
|
317
|
+
try {
|
|
318
|
+
const started: number = await $.clock.now()
|
|
319
|
+
const { root, layout } = await rootAndLayout($)
|
|
320
|
+
if (layout.kind === 'none') { $.ui.status(undefined); await update($, view, () => null); return }
|
|
321
|
+
// every process has its own limit, and the whole refresh has one: once it is spent, nothing more runs
|
|
322
|
+
const run = async (argv: string[]) =>
|
|
323
|
+
(await $.clock.now()) - started > PROCESS_LIMIT_MS ? { exitCode: 1, stdout: '', stderr: 'the view ran out of time' } : sh($, argv, { cwd: root, timeoutMs: PROCESS_LIMIT_MS })
|
|
324
|
+
|
|
325
|
+
const r: Record<string, any> = {}
|
|
326
|
+
for (const k of ['inside', 'branch', 'sha', 'dirs', 'defaultBranch'] as const) r[k] = await run(PROBES[k])
|
|
327
|
+
let g = interpretGit(r)
|
|
328
|
+
if (!g.noGit && g.branch && !g.branch.startsWith('initiative/')) { r.ahead = await run(aheadProbe(g.defaultBranch)); g = interpretGit(r) }
|
|
329
|
+
|
|
330
|
+
const idGuess = g.branch?.startsWith('initiative/') ? g.branch.slice('initiative/'.length) : null
|
|
331
|
+
const rp = idGuess ? recordPath(layout, idGuess) : null
|
|
332
|
+
const initiative = resolveInitiative({
|
|
333
|
+
branch: g.noGit ? null : g.branch,
|
|
334
|
+
recordExists: rp ? await $.fs.exists(rp) : null,
|
|
335
|
+
commitsAhead: g.commitsAhead,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
let summary: any = null
|
|
339
|
+
let unavailable: string | null = null
|
|
340
|
+
let links: { record: string | null; prd: string | null; rubric: string | null } | null = null
|
|
341
|
+
if (!g.noGit && initiative.state === 'ok' && initiative.id) {
|
|
342
|
+
const now: number = await $.clock.now()
|
|
343
|
+
const sha = g.sha ?? 'none'
|
|
344
|
+
const cached: any = await read($, factsCache)
|
|
345
|
+
if (!force && isFresh(cached, initiative.id, sha, now)) summary = cached.summary
|
|
346
|
+
else {
|
|
347
|
+
const ws = layout.kind === 'kit' ? `${root}/pmos/scripts/workflow-state.js` : `${root}/scripts/workflow-state.js`
|
|
348
|
+
if (!(await $.fs.exists(ws))) unavailable = 'workflow-state.js is not installed here'
|
|
349
|
+
else {
|
|
350
|
+
const p = parseFactsOutput(await run(['node', ws, initiative.id, '--json']))
|
|
351
|
+
if (p.ok) {
|
|
352
|
+
summary = summarize(p.json)
|
|
353
|
+
await update($, factsCache, () => ({ id: initiative.id, sha, at: now, summary }))
|
|
354
|
+
} else unavailable = p.reason
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// the record's own paths, for the slice: shown to the person, never given to the model
|
|
358
|
+
const text = rp ? await readText($, rp) : null
|
|
359
|
+
const field = (name: string): string | null => {
|
|
360
|
+
const m = text === null ? null : new RegExp(`^${name}:[ \\t]*(\\S[^\\n]*)$`, 'm').exec(text)
|
|
361
|
+
return m ? clean(m[1].replace(/^["']|["']$/g, ''), 80) : null
|
|
362
|
+
}
|
|
363
|
+
links = { record: rp ? clean(rp.replace(`${root}/`, ''), 80) : null, prd: field('prd_path'), rubric: field('rubric_path') }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const text = formatStatus({
|
|
367
|
+
repo: baseName(root),
|
|
368
|
+
worktree: g.linked ? baseName(root) : null,
|
|
369
|
+
branch: g.branch,
|
|
370
|
+
noGit: g.noGit,
|
|
371
|
+
initiative,
|
|
372
|
+
facts: summary,
|
|
373
|
+
width: lastColumns > 0 ? lastColumns : undefined,
|
|
374
|
+
})
|
|
375
|
+
$.ui.status(text)
|
|
376
|
+
const stateRel = layout.stateRoot ? layout.stateRoot.replace(`${root}/`, '') : null
|
|
377
|
+
await update($, view, () => ({ text, initiative, summary, unavailable, links, sha: g.sha, at: started, stateRel, noGit: g.noGit }))
|
|
378
|
+
} catch {
|
|
379
|
+
/* the view is a convenience: on any failure the last view stays, and the turn is not told */
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
300
383
|
const summary = (s: CockpitStatus): string => {
|
|
301
384
|
const k = s.skills
|
|
302
385
|
const lines = [
|
|
@@ -331,9 +414,11 @@ export const register: Register = (on) => {
|
|
|
331
414
|
await $.command.register({ name: 'pmos-status', description: 'Show the PMOS layout, profile and skill registration' })
|
|
332
415
|
await $.command.register({ name: 'pmos-repair-skills', description: 'Link the kit skills that are not registered in .claude/skills' })
|
|
333
416
|
await $.command.register({ name: 'pmos-grant', description: 'Mint one single-use grant: /pmos-grant merge:<PR> | publish:<artifact> [minutes] [your words]' })
|
|
417
|
+
await $.command.register({ name: 'pmos-slice', description: 'Open the initiative slice: the ladder, step by step, for the branch you are on' })
|
|
334
418
|
await $.command.register({ name: 'pmos-accept', description: 'Record your Acceptance of a run: /pmos-accept <run_id> pass|fail <note>' })
|
|
335
419
|
const s = await gather($)
|
|
336
420
|
await update($, status, () => s)
|
|
421
|
+
await refreshView($)
|
|
337
422
|
if (s.skills && s.skills.missing.length > 0) {
|
|
338
423
|
$.ui.toast(`PMOS: ${s.skills.missing.length} kit skill(s) are not registered. Run /pmos-repair-skills.`)
|
|
339
424
|
}
|
|
@@ -343,7 +428,81 @@ export const register: Register = (on) => {
|
|
|
343
428
|
on('command.run', { command: 'pmos-status' }, async ($) => {
|
|
344
429
|
const s = await gather($)
|
|
345
430
|
await update($, status, () => s)
|
|
346
|
-
|
|
431
|
+
await refreshView($, true)
|
|
432
|
+
const v: any = await read($, view)
|
|
433
|
+
const lines = [summary(s)]
|
|
434
|
+
if (v?.summary) lines.push(`stage ${v.summary.stage}${v.summary.next ? ` (next: ${v.summary.next.key})` : ''}${v.summary.gateFailures.length ? `; blocking: ${v.summary.gateFailures.join(', ')}` : ''}`)
|
|
435
|
+
else if (v?.unavailable) lines.push(`stage unavailable: ${v.unavailable}`)
|
|
436
|
+
return { text: lines.join('\n') }
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// The slice opens only when asked. Nothing in this module opens it unasked (modes, M12, come later).
|
|
440
|
+
on('command.run', { command: 'pmos-slice' }, async ($) => {
|
|
441
|
+
await refreshView($)
|
|
442
|
+
await $.ui.open({ id: 'pmos-slice', title: 'PMOS initiative' })
|
|
443
|
+
return { text: 'Opened the initiative slice. Esc closes it.' }
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
on('ui.render', { component: 'Pane', requestId: 'pmos-slice' }, async ($, e) => {
|
|
447
|
+
const { Box, Text } = $.ui.resolve(e)
|
|
448
|
+
const v: any = await read($, view)
|
|
449
|
+
if (!v || !v.initiative || v.initiative.id === null) {
|
|
450
|
+
return (
|
|
451
|
+
<Box>
|
|
452
|
+
<Text>{clean(v?.noGit ? 'No git repository here.' : v?.initiative?.text ?? 'No PMOS state here.', 120)}</Text>
|
|
453
|
+
</Box>
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
const room = Math.max(4, (e.viewport?.rows ?? 24) - 8)
|
|
457
|
+
const rows = v.summary ? sliceRows(v.summary).slice(0, room) : []
|
|
458
|
+
const when = new Date(v.at).toISOString().slice(11, 19)
|
|
459
|
+
return (
|
|
460
|
+
<Box>
|
|
461
|
+
<Text>
|
|
462
|
+
{v.initiative.id} · {v.summary ? `${v.summary.lane} · ${v.summary.stage}` : 'stage unavailable'}
|
|
463
|
+
</Text>
|
|
464
|
+
{v.unavailable && <Text>{clean(v.unavailable, 80)}</Text>}
|
|
465
|
+
{rows.map((r: any) => (
|
|
466
|
+
<Text key={r.key}>
|
|
467
|
+
{r.glyph} {r.label}
|
|
468
|
+
</Text>
|
|
469
|
+
))}
|
|
470
|
+
{v.links?.record && <Text>record {v.links.record}</Text>}
|
|
471
|
+
{v.links?.prd && <Text>prd {v.links.prd}</Text>}
|
|
472
|
+
{v.links?.rubric && <Text>rubric {v.links.rubric}</Text>}
|
|
473
|
+
<Text>
|
|
474
|
+
derived {v.sha ? String(v.sha).slice(0, 7) : 'no commit'} at {when}Z
|
|
475
|
+
</Text>
|
|
476
|
+
</Box>
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// The status line follows the branch: refreshed at the end of each turn, never per tool call.
|
|
481
|
+
on('turn.complete', async ($, e, next) => {
|
|
482
|
+
await refreshView($)
|
|
483
|
+
return next(e)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
// The one thing this mod says to the model on its own. A fixed template over allow-listed fields,
|
|
487
|
+
// read from the last view, so composing the prompt runs no process.
|
|
488
|
+
on('prompt.compose', async ($, e, next) => {
|
|
489
|
+
const r = await next(e)
|
|
490
|
+
try {
|
|
491
|
+
const v: any = await read($, view)
|
|
492
|
+
if (!v?.initiative?.id) return r
|
|
493
|
+
const text = buildSection({
|
|
494
|
+
id: v.initiative.id,
|
|
495
|
+
lane: v.summary?.lane,
|
|
496
|
+
anchor: v.summary?.anchor,
|
|
497
|
+
stage: v.summary?.stage,
|
|
498
|
+
nextKey: v.summary?.next?.key ?? null,
|
|
499
|
+
stateRoot: v.stateRel,
|
|
500
|
+
profile: v.summary?.profile,
|
|
501
|
+
})
|
|
502
|
+
return text === null ? r : { ...r, sections: [...r.sections, { id: SECTION_ID, text, scope: 'session' as const }] }
|
|
503
|
+
} catch {
|
|
504
|
+
return r
|
|
505
|
+
}
|
|
347
506
|
})
|
|
348
507
|
|
|
349
508
|
on('command.run', { command: 'pmos-repair-skills' }, async ($) => {
|
|
@@ -411,6 +570,7 @@ export const register: Register = (on) => {
|
|
|
411
570
|
})
|
|
412
571
|
|
|
413
572
|
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
573
|
+
if (typeof e.viewport?.columns === 'number') lastColumns = e.viewport.columns
|
|
414
574
|
const s = await read($, status)
|
|
415
575
|
const o: any = await read($, offer)
|
|
416
576
|
if (o && !e.props.hasSurvey) {
|
|
@@ -425,11 +585,25 @@ export const register: Register = (on) => {
|
|
|
425
585
|
)
|
|
426
586
|
}
|
|
427
587
|
if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
|
|
428
|
-
|
|
588
|
+
// one line, only when something blocks, in this order: skill drift, a blocking ladder step, a branch
|
|
589
|
+
// with commits and no initiative record. The text comes from checked fields, never from a record.
|
|
590
|
+
const v: any = await read($, view)
|
|
591
|
+
if (!hasDrift(s) && v?.initiative?.id && v?.summary?.gateFailures?.length) {
|
|
592
|
+
const { Box, Button, Text } = $.ui.resolve(e)
|
|
593
|
+
return (
|
|
594
|
+
<Box>
|
|
595
|
+
<Text>
|
|
596
|
+
PMOS: {v.initiative.id} has a blocking order violation ({v.summary.gateFailures.join(', ')}).{v.summary.next ? ` Next: ${v.summary.next.key}.` : ''}{' '}
|
|
597
|
+
</Text>
|
|
598
|
+
<Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
|
|
599
|
+
</Box>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
if (!hasDrift(s) && v?.initiative?.warn) {
|
|
429
603
|
const { Box, Button, Text } = $.ui.resolve(e)
|
|
430
604
|
return (
|
|
431
605
|
<Box>
|
|
432
|
-
<Text>PMOS: {
|
|
606
|
+
<Text>PMOS: {clean(v.initiative.text, 120)}. </Text>
|
|
433
607
|
<Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
|
|
434
608
|
</Box>
|
|
435
609
|
)
|
|
@@ -27,8 +27,34 @@ export type CockpitStatus = {
|
|
|
27
27
|
repairError: string | null
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export type ViewState = {
|
|
31
|
+
text: string
|
|
32
|
+
initiative: InitiativeFact
|
|
33
|
+
summary: {
|
|
34
|
+
id: string | null
|
|
35
|
+
lane: string
|
|
36
|
+
stage: string
|
|
37
|
+
profile: string
|
|
38
|
+
anchor: string | null
|
|
39
|
+
accepted: boolean
|
|
40
|
+
stopped: boolean
|
|
41
|
+
gateFailures: string[]
|
|
42
|
+
blockers: string[]
|
|
43
|
+
next: { key: string } | null
|
|
44
|
+
steps: { key: string; label: string; status: string }[]
|
|
45
|
+
} | null
|
|
46
|
+
unavailable: string | null
|
|
47
|
+
links: { record: string | null; prd: string | null; rubric: string | null } | null
|
|
48
|
+
sha: string | null
|
|
49
|
+
at: number
|
|
50
|
+
stateRel: string | null
|
|
51
|
+
noGit: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type FactsCache = { id: string | null; sha: string; at: number; summary: NonNullable<ViewState['summary']> }
|
|
55
|
+
|
|
30
56
|
declare module 'claude-code' {
|
|
31
57
|
interface PluginState {
|
|
32
|
-
'pmos-cockpit': { status: CockpitStatus | null; isHidden: boolean; offer: { run_id: string; initiative_id: string; message: string } | null }
|
|
58
|
+
'pmos-cockpit': { status: CockpitStatus | null; isHidden: boolean; offer: { run_id: string; initiative_id: string; message: string } | null; view: ViewState | null; factsCache: FactsCache | null }
|
|
33
59
|
}
|
|
34
60
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-04T09:05:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-04T09:05:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-04T09:05:06Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/package.json
CHANGED