create-pmos 0.38.4 → 0.38.6
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 +2 -2
- package/payload/okf/core/templates/requirements-brief-template.md +61 -0
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/lane-defs.js +226 -0
- package/payload/scripts/workflow-state.js +108 -24
- package/payload/scripts/workflow-sweep.js +155 -3
- package/payload/skills/design-brief.skill +5 -6
- package/payload/skills/discovery.skill +1 -2
- package/payload/skills/eval-rubric.skill +2 -5
- package/payload/skills/pmos-router.skill +3 -2
- package/payload/skills/prd.skill +7 -13
- package/payload/skills/requirements-elicitation.skill +231 -0
- package/payload/skills-native/design-brief/SKILL.md +5 -6
- package/payload/skills-native/discovery/SKILL.md +1 -2
- package/payload/skills-native/eval-rubric/SKILL.md +2 -5
- package/payload/skills-native/pmos-router/SKILL.md +3 -2
- package/payload/skills-native/prd/SKILL.md +7 -13
- package/payload/skills-native/requirements-elicitation/SKILL.md +233 -0
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/lib/audit.js +54 -0
- package/workspace/lib/engine/lane-defs.js +226 -0
- package/workspace/lib/engine/workflow-state.js +108 -24
- package/workspace/lib/engine/workflow-sweep.js +934 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +2 -2
- package/workspace/public/pm/data.js +67 -24
- package/workspace/public/pm/panel-detail.js +12 -2
- package/workspace/public/pm/panel-repos.js +36 -5
- package/workspace/public/pm/tab-task.js +4 -1
- package/workspace/server.js +22 -1
- package/workspace/skills/gather-requirements.skill +13 -1
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.6",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test:parity": "node design-parity.js --states",
|
|
16
16
|
"test:all": "npm test && npm run test:panels && npm run test:ui && npm run test:parity && npm run test:offline",
|
|
17
17
|
"doctor": "node doctor.js",
|
|
18
|
-
"test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js",
|
|
18
|
+
"test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js",
|
|
19
19
|
"test:offline": "node offline-tarball.test.js"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
@@ -110,43 +110,55 @@ const RUNGS = {
|
|
|
110
110
|
feature: {
|
|
111
111
|
intake: ['route', 'anchor', 'prototype', 'understand', 'priors'],
|
|
112
112
|
spec: ['spec'], contract: ['contract'], build: ['build', 'quality'],
|
|
113
|
-
eval: ['review', 'explain'], accepted: ['accept'],
|
|
113
|
+
eval: ['review', 'explain'], accepted: ['accept', 'outcome'],
|
|
114
114
|
},
|
|
115
115
|
discovery: {
|
|
116
116
|
intake: ['route', 'anchor', 'prototype', 'understand', 'priors', 'spec', 'contract'],
|
|
117
|
-
explore: ['build', 'quality'], synthesize: ['review'], decide: ['explain', 'accept'],
|
|
117
|
+
explore: ['build', 'quality'], synthesize: ['review'], decide: ['explain', 'accept', 'outcome'],
|
|
118
118
|
},
|
|
119
119
|
chore: {
|
|
120
120
|
intake: ['route', 'anchor', 'prototype', 'understand', 'priors', 'spec', 'contract'],
|
|
121
|
-
fix: ['build', 'quality'], verify: ['review', 'explain', 'accept'],
|
|
121
|
+
fix: ['build', 'quality'], verify: ['review', 'explain', 'accept', 'outcome'],
|
|
122
122
|
},
|
|
123
123
|
incident: {
|
|
124
124
|
intake: ['route', 'anchor', 'prototype', 'understand', 'priors'],
|
|
125
|
-
contain: ['build', 'quality'], record: ['spec', 'contract', 'review'], verify: ['explain', 'accept'],
|
|
125
|
+
contain: ['build', 'quality'], record: ['spec', 'contract', 'review'], verify: ['explain', 'accept', 'outcome'],
|
|
126
126
|
},
|
|
127
127
|
content: {
|
|
128
128
|
intake: ['route', 'anchor', 'prototype', 'understand', 'priors'],
|
|
129
|
-
draft: ['spec', 'contract', 'build', 'quality'], review: ['review'], publish: ['explain', 'accept'],
|
|
129
|
+
draft: ['spec', 'contract', 'build', 'quality'], review: ['review'], publish: ['explain', 'accept', 'outcome'],
|
|
130
130
|
},
|
|
131
131
|
prototype: {
|
|
132
132
|
intake: ['route', 'anchor', 'prototype', 'understand', 'priors', 'spec', 'contract'],
|
|
133
|
-
specimens: ['build', 'quality'], grill: ['review', 'explain'], dispose: ['accept'],
|
|
133
|
+
specimens: ['build', 'quality'], grill: ['review', 'explain'], dispose: ['accept', 'outcome'],
|
|
134
134
|
},
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
/* D68 — the pm-artifacts profile's ladder: every
|
|
138
|
-
* (→ handed off). Keyed `pm:<lane>` because `discovery` and `chore` exist in BOTH profiles
|
|
139
|
-
* different ladders; the profile comes from the engine's own ladder row, never guessed.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
/* D68/D77 — the pm-artifacts profile's ladder: every document lane walks brief → draft → review →
|
|
138
|
+
* approved (→ handed off). Keyed `pm:<lane>` because `discovery` and `chore` exist in BOTH profiles
|
|
139
|
+
* with different ladders; the profile comes from the engine's own ladder row, never guessed.
|
|
140
|
+
*
|
|
141
|
+
* The lane NAMES and whether a lane hands off are not written here. They come from the server's
|
|
142
|
+
* lane definitions (scripts/lane-defs.js, D77) through `setLaneDefs`, so a lane added to the
|
|
143
|
+
* definitions has phases with no edit in this file. Before the first payload arrives, and for a
|
|
144
|
+
* lane the server did not name, the phases are `pm:prd`'s — the profile's strictest. */
|
|
145
|
+
let laneDefsFromServer = null;
|
|
146
|
+
function buildPmLane(l, withHandoff) {
|
|
144
147
|
PHASES['pm:' + l] = ['intake', 'brief', 'draft', 'review', 'approved'].concat(withHandoff ? ['handed off'] : []);
|
|
145
148
|
RUNGS['pm:' + l] = Object.assign({ intake: ['route', 'anchor'], brief: ['brief'], draft: ['draft'], review: ['review'], approved: ['approved'] },
|
|
146
149
|
withHandoff ? { 'handed off': ['handoff'] } : {});
|
|
147
150
|
}
|
|
151
|
+
buildPmLane('prd', true);
|
|
152
|
+
/** Register the lane definitions the server sent with an initiative payload. */
|
|
153
|
+
function setLaneDefs(defs) {
|
|
154
|
+
if (!defs || typeof defs !== 'object') return;
|
|
155
|
+
laneDefsFromServer = defs;
|
|
156
|
+
for (const [l, d] of Object.entries(defs)) {
|
|
157
|
+
if (Array.isArray(d.rungs)) buildPmLane(l, d.rungs.includes('handoff'));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
148
160
|
const laneOf = (lane, profile) => (profile === 'pm-artifacts'
|
|
149
|
-
? 'pm:' + (
|
|
161
|
+
? 'pm:' + (PHASES['pm:' + lane] ? lane : 'prd')
|
|
150
162
|
: (PHASES[lane] ? lane : 'feature'));
|
|
151
163
|
|
|
152
164
|
/**
|
|
@@ -166,7 +178,8 @@ function phaseRungStatus(rungs) {
|
|
|
166
178
|
if (rungs.every((r) => !r.evidence)) return 'pending';
|
|
167
179
|
if (rungs.some((r) => r.status === 'blocked')) return 'blocked';
|
|
168
180
|
if (rungs.some((r) => r.status === 'current')) return 'current';
|
|
169
|
-
|
|
181
|
+
// An outcome still waiting on its horizon is time, not unfinished work: it does not hold the phase open.
|
|
182
|
+
const decided = rungs.filter((r) => r.status !== 'na' && r.status !== 'unknown' && !(r.key === 'outcome' && r.status === 'pending'));
|
|
170
183
|
if (decided.length && decided.every((r) => r.status === 'done')) return 'done';
|
|
171
184
|
return 'pending';
|
|
172
185
|
}
|
|
@@ -200,8 +213,21 @@ function derivedTasks(phase, rungs, ctx) {
|
|
|
200
213
|
? ['resolve the ' + phase + ' blocker', 'produce the ' + phase + ' output', 'record the result']
|
|
201
214
|
: ['read the inputs for ' + phase, 'produce the ' + phase + ' output', 'record the result'];
|
|
202
215
|
const evidence = rungs.filter((r) => r.evidence).map((r) => r.key + ': ' + r.evidence).join(' · ') || null;
|
|
203
|
-
const inputs = [ctx.record, ctx.prd, ctx.rubric].filter(Boolean)
|
|
216
|
+
const inputs = [ctx.record, ctx.brief, ctx.doc, ctx.prd, ctx.rubric].filter(Boolean)
|
|
217
|
+
.filter((v, i, a) => a.indexOf(v) === i).join(' · ') || null;
|
|
218
|
+
// D77 — what each rung read and the sentence that decides it, from the engine's own basis.
|
|
219
|
+
const basis = rungs.map((r) => ({ key: r.key, b: r.basis || null })).filter((x) => x.b);
|
|
220
|
+
const sentences = basis.map((x) => x.b.rule).filter((v, i, a) => v && a.indexOf(v) === i);
|
|
221
|
+
const readLine = basis.filter((x) => x.b.file)
|
|
222
|
+
.map((x) => x.b.file + (x.b.first_seen ? ' (first seen ' + x.b.first_seen + ')' : '')).filter((v, i, a) => a.indexOf(v) === i);
|
|
204
223
|
const rules = rungs.map((r) => r.rule).filter(Boolean);
|
|
224
|
+
// The produced artifact: a file a rung of this phase read, never guessed. A document or record is
|
|
225
|
+
// preferred over a ledger for the second task; the third task shows the ledger the result lands in.
|
|
226
|
+
const files = basis.filter((x) => x.b.file && x.b.source === 'file').map((x) => x.b.file);
|
|
227
|
+
const ledgers = basis.filter((x) => x.b.file && x.b.source === 'ledger').map((x) => x.b.file);
|
|
228
|
+
const produced = files[files.length - 1] || pathIn(lead.evidence) || pathIn(lead.next) || null;
|
|
229
|
+
const recorded = ledgers[ledgers.length - 1] || null;
|
|
230
|
+
const def = ctx.laneDef || null;
|
|
205
231
|
return titles.map((title, i) => ({
|
|
206
232
|
title,
|
|
207
233
|
status: (TASK_STATUS[s] || TASK_STATUS.pending)[i],
|
|
@@ -210,13 +236,14 @@ function derivedTasks(phase, rungs, ctx) {
|
|
|
210
236
|
// engine's sentence, the other is a list of paths the record declares.
|
|
211
237
|
instruction: (i === 0 && inputs ? 'Read ' + inputs : null) || lead.next || lead.evidence || null,
|
|
212
238
|
input: inputs,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
239
|
+
// What the lane says finishing means, then what the engine found. Both are data, not prose written here.
|
|
240
|
+
expect: [def ? def.done : null, evidence ? 'Found: ' + evidence : null].filter(Boolean).join(' — ') || null,
|
|
241
|
+
evalNote: sentences.length
|
|
242
|
+
? sentences.join(' ') + (readLine.length ? ' Decided from: ' + readLine.join(', ') + '.' : '')
|
|
243
|
+
: rules.length
|
|
244
|
+
? 'workflow rule ' + rules.join(', ') + ' decides this phase'
|
|
245
|
+
: 'the workflow engine derives this phase from ' + rungs.map((r) => r.key).join(', '),
|
|
246
|
+
output: i === 1 ? produced : (i === 2 ? recorded : null),
|
|
220
247
|
}));
|
|
221
248
|
}
|
|
222
249
|
|
|
@@ -385,11 +412,16 @@ function recordFrom(payload) {
|
|
|
385
412
|
return { readError: error, l2: ['read failed', error.slice(0, 60), C.des] };
|
|
386
413
|
}
|
|
387
414
|
const fm = payload.frontmatter || {};
|
|
415
|
+
if (payload.lanes && payload.lanes.defs) setLaneDefs(payload.lanes.defs);
|
|
388
416
|
const row = (payload.ladder && payload.ladder.ok && payload.ladder.row) || null;
|
|
389
417
|
const scope = (payload.ladder && payload.ladder.scope) || null;
|
|
390
418
|
const lane = fm.lane || (row && row.lane) || null;
|
|
391
419
|
const steps = row ? phaseSteps(lane, row.steps, row.profile) : [];
|
|
392
|
-
const
|
|
420
|
+
const laneDef = (payload.lanes && payload.lanes.defs && lane && payload.lanes.defs[lane]) || null;
|
|
421
|
+
const ctx = {
|
|
422
|
+
record: payload.record, prd: fm.prd_path || null, rubric: fm.rubric_path || null,
|
|
423
|
+
brief: fm.brief_path || null, doc: fm.doc_path || null, laneDef,
|
|
424
|
+
};
|
|
393
425
|
// Authored tasks win over derivation, per phase (PRD, "Tasks"). Their absence is not a defect.
|
|
394
426
|
const authored = payload.tasks || {};
|
|
395
427
|
const tasks = {};
|
|
@@ -406,6 +438,8 @@ function recordFrom(payload) {
|
|
|
406
438
|
// the lane this record declares, and the lanes this repository allows — what the header's lane picker offers
|
|
407
439
|
lane: lane || null,
|
|
408
440
|
lanes: payload.lanes && Array.isArray(payload.lanes.lanes) ? payload.lanes.lanes : null,
|
|
441
|
+
laneDefs: (payload.lanes && payload.lanes.defs) || null,
|
|
442
|
+
laneDef,
|
|
409
443
|
stage: row ? row.stage : null,
|
|
410
444
|
flow: fm.flow || null,
|
|
411
445
|
pills: pillsFor({ lane, anchor: payload.anchor, flow: fm.flow || null }),
|
|
@@ -577,6 +611,7 @@ function foldWorkspaces(rows) {
|
|
|
577
611
|
// here and never re-derived: a second opinion about what a repository IS would be a second
|
|
578
612
|
// thing to keep true, and this one is the one that did the read.
|
|
579
613
|
kind: w.kind === 'github' ? 'github' : 'local', remote: w.remote || null,
|
|
614
|
+
laneSets: w.lane_sets || null,
|
|
580
615
|
});
|
|
581
616
|
}
|
|
582
617
|
const p = byRepo.get(key);
|
|
@@ -1059,6 +1094,14 @@ export async function removeRepository(name) {
|
|
|
1059
1094
|
export function repoInitials(name) { const p = products.get(name); return (p && p.initials) || null; }
|
|
1060
1095
|
|
|
1061
1096
|
/** panel-repos.js — the repository's profile (D68): 'pm-artifacts' (documents) or 'product-code'. */
|
|
1097
|
+
/** The workflow audit for one repository (D77): the sweep, read-only. Resolves to the report or `{ok:false,error}`. */
|
|
1098
|
+
export async function loadAudit(name) {
|
|
1099
|
+
const p = products.get(name);
|
|
1100
|
+
if (!p) return { ok: false, error: 'that repository is not registered' };
|
|
1101
|
+
return getJson('/api/audit', { ws: p.ws });
|
|
1102
|
+
}
|
|
1103
|
+
/** The lane names a profile offers, as the server's engine reports them; null when it did not say. */
|
|
1104
|
+
export function repoLanes(name, profile) { const p = products.get(name); return (p && p.laneSets && p.laneSets[profile]) || null; }
|
|
1062
1105
|
export function repoProfile(name) { const p = products.get(name); return (p && p.profile) || 'product-code'; }
|
|
1063
1106
|
|
|
1064
1107
|
/** panel-repos.js — write or remove `.pmos-profile`. Refusals come back as { ok:false, error }. */
|
|
@@ -295,7 +295,8 @@ let laneNote = null;
|
|
|
295
295
|
function lanePicker(slug, rec) {
|
|
296
296
|
if (FIXTURE || !rec || !rec.lane || !Array.isArray(rec.lanes) || !rec.lanes.length) return null;
|
|
297
297
|
const opts = rec.lanes.includes(rec.lane) ? rec.lanes : [rec.lane, ...rec.lanes];
|
|
298
|
-
|
|
298
|
+
const cur = rec.laneDefs && rec.laneDefs[rec.lane];
|
|
299
|
+
const bar = el('div', { 'data-r15': 'lane-bar', style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:8px;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' },
|
|
299
300
|
el('span', null, 'workflow'),
|
|
300
301
|
el('select', { 'data-r15': 'lane-pick', title: 'change this initiative’s workflow (lane); the ladder re-derives from it',
|
|
301
302
|
style: 'font-family:var(--mono);font-size:10.5px;background:var(--secondary);border:1px solid var(--border);color:var(--foreground);padding:2px 4px;min-height:24px',
|
|
@@ -310,8 +311,17 @@ function lanePicker(slug, rec) {
|
|
|
310
311
|
laneNote = out && out.ok ? 'lane is now ' + next : 'not changed — ' + ((out && out.error) || 'the server did not answer');
|
|
311
312
|
setState({});
|
|
312
313
|
} },
|
|
313
|
-
opts.map((l) =>
|
|
314
|
+
opts.map((l) => {
|
|
315
|
+
const d = rec.laneDefs && rec.laneDefs[l];
|
|
316
|
+
return el('option', { value: l, title: d ? d.when : null, selected: l === rec.lane ? 'selected' : null }, d && d.label ? d.label + ' (' + l + ')' : l);
|
|
317
|
+
})),
|
|
314
318
|
laneNote ? el('span', { 'data-r15': 'lane-note', style: 'color:var(--muted-foreground)' }, iv(laneNote)) : null);
|
|
319
|
+
if (!cur) return bar;
|
|
320
|
+
// D77 — what this lane is: the definition the engine runs on, read from the same module.
|
|
321
|
+
const row = (k, v) => el('div', null, el('span', { style: 'text-transform:uppercase;letter-spacing:.1em;font-size:9.5px;margin-right:6px' }, k), iv(v));
|
|
322
|
+
return el('div', null, bar,
|
|
323
|
+
el('div', { 'data-r17': 'lane-def', style: 'margin-top:6px;font-size:11.5px;line-height:1.5;color:var(--muted-foreground)' },
|
|
324
|
+
row('when', cur.when), row('input', cur.requires || cur.input), row('output', cur.output), row('done', cur.done)));
|
|
315
325
|
}
|
|
316
326
|
|
|
317
327
|
function headerBlock(name, tag, pills, crumb, lane) {
|
|
@@ -276,14 +276,41 @@ async function renameRepo(name) {
|
|
|
276
276
|
|
|
277
277
|
/* The repository's profile (D68). `pm-artifacts` offers document lanes and no build rungs; `product-code` is the
|
|
278
278
|
* default. The confirm states the consequence before anything is written. */
|
|
279
|
-
const
|
|
280
|
-
const
|
|
279
|
+
const lanesText = (name, profile) => {
|
|
280
|
+
const l = dataMod && dataMod.repoLanes ? dataMod.repoLanes(name, profile) : null;
|
|
281
|
+
return l ? l.join(', ') : '(the workspace did not report them)';
|
|
282
|
+
};
|
|
283
|
+
/* The workflow audit (D77). Runs the sweep on the repository and shows what it found. It writes nothing. */
|
|
284
|
+
async function runAudit(name) {
|
|
285
|
+
if (!dataMod || !dataMod.loadAudit) return;
|
|
286
|
+
setState({ repoAudit: { name, busy: true } });
|
|
287
|
+
const out = await dataMod.loadAudit(name);
|
|
288
|
+
setState({ repoAudit: { name, report: out && out.ok !== false ? out : null, error: out && out.ok === false ? out.error : null } });
|
|
289
|
+
}
|
|
290
|
+
function auditBlock(name) {
|
|
291
|
+
const a = state.repoAudit;
|
|
292
|
+
if (!a || a.name !== name) return null;
|
|
293
|
+
const box = (kids) => el('div', { 'data-r17': 'audit-report', style: 'padding:6px 14px 10px 39px;font-size:11px;line-height:1.5;color:var(--muted-foreground)' }, kids);
|
|
294
|
+
if (a.busy) return box(ip('checking…'));
|
|
295
|
+
if (a.error || !a.report) return box(ip('the audit could not run — ' + (a.error || 'no answer')));
|
|
296
|
+
const r = a.report;
|
|
297
|
+
const head = r.counts.defects + ' defect' + (r.counts.defects === 1 ? '' : 's') + ' · ' + r.counts.owed + ' owed · ' + r.counts.repairable + ' repairable';
|
|
298
|
+
const line = (f) => el('div', { style: 'margin-top:4px', 'data-r17': 'finding', 'data-class': f.class },
|
|
299
|
+
el('span', { style: 'font-family:var(--mono);color:var(--foreground)' }, ip(f.class)), ip(' · ' + (f.id || 'repository') + ' — ' + f.what),
|
|
300
|
+
f.repair ? el('div', null, ip('repair: set ' + f.repair.key + ' to ' + f.repair.to + ' — ' + f.command)) : (f.hand ? el('div', null, ip(f.hand)) : null));
|
|
301
|
+
return box([
|
|
302
|
+
el('div', { style: 'color:var(--foreground)', 'data-r17': 'audit-counts' }, ip(head)),
|
|
303
|
+
r.defects.slice(0, 8).map(line),
|
|
304
|
+
r.counts.defects > 8 ? el('div', { style: 'margin-top:4px' }, ip('+ ' + (r.counts.defects - 8) + ' more — node scripts/workflow-sweep.js')) : null,
|
|
305
|
+
r.counts.owed ? el('div', { style: 'margin-top:6px' }, ip('owed (open loops, not corruption): ' + Object.entries(r.owed_by_class).map(([k, v]) => k + ' ×' + v).join(', '))) : null,
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
281
308
|
async function switchProfile(name) {
|
|
282
309
|
if (!dataMod || !dataMod.repoProfile) return;
|
|
283
310
|
const toDocs = dataMod.repoProfile(name) !== 'pm-artifacts';
|
|
284
311
|
const msg = toDocs
|
|
285
|
-
? 'Switch “' + name + '” to document work?\n\nLanes offered: ' +
|
|
286
|
-
: 'Switch “' + name + '” to code work?\n\nLanes offered: ' +
|
|
312
|
+
? 'Switch “' + name + '” to document work?\n\nLanes offered: ' + lanesText(name, 'pm-artifacts') + '.\nThe ladder becomes route, anchor, brief, draft, review, approved, handed off — no contract, build, eval or explainer.'
|
|
313
|
+
: 'Switch “' + name + '” to code work?\n\nLanes offered: ' + lanesText(name, 'product-code') + '.\nThe ladder returns to spec, contract, build, eval, explainer, acceptance.';
|
|
287
314
|
const tail = '\n\nAn initiative whose lane is not in the new set is treated as the strictest lane (' + (toDocs ? 'a governed prd' : 'feature') + '). No initiative record is edited.\n\nThis ' + (toDocs ? 'writes' : 'removes') + ' the file .pmos-profile in the repository. It is a change for you to commit.';
|
|
288
315
|
const ok = typeof window === 'undefined' || !window.confirm ? true : window.confirm(msg + tail);
|
|
289
316
|
if (!ok) return;
|
|
@@ -323,9 +350,12 @@ function repoEntry(tuple, product) {
|
|
|
323
350
|
{ const sb = syncButton(name); if (sb) tools.push(sb); }
|
|
324
351
|
{ const docs = dataMod.repoProfile && dataMod.repoProfile(name) === 'pm-artifacts';
|
|
325
352
|
tools.push(el('button', { onClick: () => switchProfile(name), style: chipStyle, hover: 'color:var(--foreground)',
|
|
326
|
-
title: docs ? 'document work (pm-artifacts): lanes are
|
|
353
|
+
title: docs ? 'document work (pm-artifacts): lanes are kinds of PM work and documents, with no build rungs. Press to switch to code work.'
|
|
327
354
|
: 'code work (product-code): lanes are feature, discovery, chore … with the full build ladder. Press to switch to document work.',
|
|
328
355
|
'data-r16': 'profile', 'data-profile': docs ? 'pm-artifacts' : 'product-code' }, ip(docs ? 'docs' : 'code'))); }
|
|
356
|
+
tools.push(el('button', { onClick: () => runAudit(name), style: chipStyle, hover: 'color:var(--foreground)',
|
|
357
|
+
title: 'check this repository’s workflow records against its files and its profile (read-only — nothing is written)',
|
|
358
|
+
'data-r17': 'audit' }, ip('audit')));
|
|
329
359
|
tools.push(el('button', { onClick: () => renameRepo(name), style: actStyle, hover: 'color:var(--foreground)', title: 'rename this repository', 'data-r1': 'rename' }, '✎'));
|
|
330
360
|
tools.push(el('button', { onClick: () => removeRepo(name), style: actStyle, hover: 'color:var(--destructive)', title: 'remove this repository from PMOS (no folder is deleted)', 'data-r1': 'remove' }, '✕'));
|
|
331
361
|
}
|
|
@@ -335,6 +365,7 @@ function repoEntry(tuple, product) {
|
|
|
335
365
|
{ const bf = branchForm(name); if (bf) kids.push(bf); }
|
|
336
366
|
if (open && trees.length) kids.push(treesBlock(name, trees, product === name));
|
|
337
367
|
{ const tn = treeNoteFor(name); if (tn) kids.push(tn); }
|
|
368
|
+
{ const ab = auditBlock(name); if (ab) kids.push(ab); }
|
|
338
369
|
const n = state.repoNotice;
|
|
339
370
|
if (n && n.name === name) {
|
|
340
371
|
kids.push(el('div', {
|
|
@@ -49,7 +49,10 @@ export function mountTask(host, tab) {
|
|
|
49
49
|
iv(tab.output),
|
|
50
50
|
)
|
|
51
51
|
: el('div', { style: 'font-size:13px;color:var(--muted-foreground);font-style:italic' },
|
|
52
|
-
|
|
52
|
+
// Only a step that is not done is "not produced yet". A done step with no file says what decided it.
|
|
53
|
+
tab.status === 'done'
|
|
54
|
+
? 'this step read no file of its own — the rule and evidence above are its result'
|
|
55
|
+
: 'no artifact yet — produced when the agent runs this task');
|
|
53
56
|
|
|
54
57
|
const body = el('div', {
|
|
55
58
|
style: 'flex:1;min-height:0;overflow-y:auto;background:var(--background);color:var(--foreground);padding:22px 26px 60px;font-size:14px;line-height:1.65',
|
package/workspace/server.js
CHANGED
|
@@ -74,6 +74,7 @@ const fmParse = require('./lib/file-mode-parse'); // canonical file-mode pa
|
|
|
74
74
|
const skillsReg = require('./lib/skills-registry'); // discovered and generated skills: quarantine, provenance, local approval (D76)
|
|
75
75
|
const modelLib = require('./lib/models'); // tier routing — one code path, shared with the agents' CLI (D75)
|
|
76
76
|
const memoryLib = require('./lib/memory'); // curated repository memory — one ledger code path, shared with the agents' CLI (D72)
|
|
77
|
+
const audit = require('./lib/audit'); // the workflow audit — the sweep the agents' CLI runs, read-only here (D77)
|
|
77
78
|
const syncLib = require('./lib/sync'); // press-only fetch / integrate / commit records / push (D70)
|
|
78
79
|
const gitInfo = require('./lib/git-info'); // read-mostly branch awareness + select/create at session start
|
|
79
80
|
const startPolicy = require('./lib/start-policy'); // ONE stated rule for where a session starts (repo-first R1/R3)
|
|
@@ -591,11 +592,23 @@ function profileOfRoot(root) {
|
|
|
591
592
|
return line === 'pm-artifacts' ? 'pm-artifacts' : 'product-code';
|
|
592
593
|
} catch { return 'product-code'; }
|
|
593
594
|
}
|
|
595
|
+
// D77 — the lane names of each profile, from the engine's own definitions. The client never carries a list.
|
|
596
|
+
let laneSetsMemo = null;
|
|
597
|
+
function laneSets() {
|
|
598
|
+
if (laneSetsMemo) return laneSetsMemo;
|
|
599
|
+
try {
|
|
600
|
+
const e = consoleState.loadEngine();
|
|
601
|
+
if (e && e.ok && typeof e.engine.laneNames === 'function') {
|
|
602
|
+
laneSetsMemo = { 'pm-artifacts': e.engine.laneNames('pm-artifacts'), 'product-code': e.engine.laneNames('product-code') };
|
|
603
|
+
}
|
|
604
|
+
} catch { /* the engine is not here; the client then says so rather than guessing */ }
|
|
605
|
+
return laneSetsMemo;
|
|
606
|
+
}
|
|
594
607
|
function wsList() {
|
|
595
608
|
return [...workspaces.values()].map((w) => {
|
|
596
609
|
const rid = repoIdentity(w.root);
|
|
597
610
|
return {
|
|
598
|
-
id: w.id, name: w.name, label: w.label || null, initials: w.initials || null, profile: profileOfRoot(w.root), root: w.root, home: HOME_ENABLED && w.root === SELF_ROOT,
|
|
611
|
+
id: w.id, name: w.name, label: w.label || null, initials: w.initials || null, profile: profileOfRoot(w.root), lane_sets: laneSets(), root: w.root, home: HOME_ENABLED && w.root === SELF_ROOT,
|
|
599
612
|
shape: wsShape(w.root), addedAt: w.addedAt,
|
|
600
613
|
sessions: [...sessions.values()].filter((s) => s.ws === w.id).length,
|
|
601
614
|
// the repository this working tree belongs to, and the branch it is on. `branch_code`
|
|
@@ -5178,6 +5191,14 @@ async function handleRequest(req, res) {
|
|
|
5178
5191
|
try { if (typeof engine.engine.lanesFor === 'function') lanes = engine.engine.lanesFor(root); } catch { lanes = null; }
|
|
5179
5192
|
return json(res, { ws: w ? w.id : null, ...out, lanes });
|
|
5180
5193
|
}
|
|
5194
|
+
if (p === '/api/audit' && req.method === 'GET') {
|
|
5195
|
+
/* The workflow audit (D77): the sweep run on one repository, read-only. It reports state that contradicts
|
|
5196
|
+
* the repository (a document no record points at, a lane the profile does not offer, a done step with
|
|
5197
|
+
* nothing to open) and work honestly not done. It writes nothing; repairs are the CLI's, on the PM's word. */
|
|
5198
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
5199
|
+
const root = w ? w.root : SELF_ROOT;
|
|
5200
|
+
return json(res, audit.run(root));
|
|
5201
|
+
}
|
|
5181
5202
|
if (p === '/api/initiative/lane' && req.method === 'POST') {
|
|
5182
5203
|
/* Change an existing initiative's lane — the PM's word. The ladder is DERIVED from `lane:` (workflow-state.js),
|
|
5183
5204
|
* so this edits that one frontmatter line and nothing else; the next read re-derives the rungs. It never
|
|
@@ -4,6 +4,10 @@ label: "Gather requirements"
|
|
|
4
4
|
description: "Read what changed in the PM's mail, chat, Jira, Confluence, calendar and meeting notes since the last note, and file one dated note of requirements — each with its source, how sure it is, and which initiative it touches."
|
|
5
5
|
run: agent
|
|
6
6
|
agent: claude
|
|
7
|
+
version: 1.1.0
|
|
8
|
+
owner: wawan
|
|
9
|
+
risk: low
|
|
10
|
+
scope: read:connectors, read:state, write:state/requirements
|
|
7
11
|
output:
|
|
8
12
|
- New or changed requirements
|
|
9
13
|
- Validations and contradictions
|
|
@@ -20,17 +24,22 @@ Rules that always hold:
|
|
|
20
24
|
4. NOTHING INVENTED. If a source is not reachable in this session, say so under "Sources reached and not reached". If nothing changed, say that. An honest empty note is correct.
|
|
21
25
|
5. UNCONFIRMED IS ALLOWED. Mark every item `confidence: unconfirmed` or `confirmed` (confirmed = a person with authority said it in a decision, an approved document or a closed ticket). A note is evidence, never a decision or an acceptance.
|
|
22
26
|
|
|
23
|
-
Procedure:
|
|
27
|
+
Procedure (each step ends with a check that shows it is done):
|
|
24
28
|
1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. The note goes in `<state>/requirements/YYYY-MM-DD.md` (today's date). If today's file already exists, do NOT overwrite it: append a section `## Later run HH:MM`.
|
|
29
|
+
Done when: the target path is named, and an existing file is still intact.
|
|
25
30
|
2. Read the previous note in that folder to learn the cutoff. With no previous note, look back 3 days.
|
|
31
|
+
Done when: you can state the cutoff as a date and time.
|
|
26
32
|
3. Read the initiatives in `<state>/initiatives/` (or `planning/initiatives/`) so you can say which one an item touches. Match by what the item is about, not by keyword.
|
|
33
|
+
Done when: you have the list of initiative ids that an item can name.
|
|
27
34
|
4. For each source you can reach, read what changed since the cutoff: mail, chat, Jira, Confluence, calendar events with their notes, meeting notes.
|
|
35
|
+
Done when: each source kind is either read or marked not reached.
|
|
28
36
|
5. For each requirement found, write one item:
|
|
29
37
|
- the requirement, in your words
|
|
30
38
|
- pointer: kind · id or link · date · author
|
|
31
39
|
- confidence: unconfirmed | confirmed
|
|
32
40
|
- relation: validates | clarifies | extends | contradicts | none
|
|
33
41
|
- initiative: its id, or `unmatched`
|
|
42
|
+
Done when: every item has all five fields and a pointer.
|
|
34
43
|
6. Start the file with this frontmatter (OKF v0.2, D73). Every key is on ONE line — never indent a key under another, because the readers stop at the first indented line. Then the four sections above, in this order:
|
|
35
44
|
```
|
|
36
45
|
---
|
|
@@ -43,8 +52,11 @@ Procedure:
|
|
|
43
52
|
---
|
|
44
53
|
```
|
|
45
54
|
`sources` lists every item the note is derived from (the same pointers you give per item). Do not add a `timestamp` and do not add `verified`: only the PM confirms a note.
|
|
55
|
+
Done when: the file starts with this frontmatter and holds the four sections in order.
|
|
46
56
|
7. Under "Sources reached and not reached", list every source kind you tried, with `reached` or `not reached — <why>`, and the number of private items skipped.
|
|
57
|
+
Done when: every source kind you tried has a line, and the private count is stated.
|
|
47
58
|
8. Finish by saying in one line how many items you filed and where. Do not commit; the PM's Sync carries the file.
|
|
59
|
+
Done when: the count and the path are said once, and no commit was made.
|
|
48
60
|
|
|
49
61
|
Output contract — produce EXACTLY these sections, the same shape every run:
|
|
50
62
|
- New or changed requirements
|