create-pmos 0.42.0 → 0.44.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/bin/cockpit-install.js +130 -0
- package/bin/create-pmos.js +47 -4
- package/package.json +1 -1
- package/payload/AGENTS-HUMAN.md +10 -25
- package/payload/AGENTS.md +4 -0
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/.claude-plugin/marketplace.json +12 -0
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +9 -0
- package/payload/mods/pmos-cockpit/README.md +60 -0
- package/payload/mods/pmos-cockpit/hooks/hooks.json +1 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +53 -0
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +88 -0
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +561 -0
- package/payload/mods/pmos-cockpit/hooks/lib/initiative.mjs +54 -0
- package/payload/mods/pmos-cockpit/hooks/lib/layout.mjs +31 -0
- package/payload/mods/pmos-cockpit/hooks/lib/profile.mjs +17 -0
- package/payload/mods/pmos-cockpit/hooks/lib/skills.mjs +69 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +460 -0
- package/payload/mods/pmos-cockpit/types/index.d.ts +34 -0
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +119 -7
- package/payload/skills/pmos-router.skill +1 -1
- package/payload/skills/prototype.skill +1 -1
- package/payload/skills/strategic-brief.skill +1 -1
- package/payload/skills-native/harness-refine/SKILL.md +1 -1
- package/payload/skills-native/pmos-router/SKILL.md +1 -1
- package/payload/skills-native/prototype/SKILL.md +1 -1
- package/payload/skills-native/strategic-brief/SKILL.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +4 -0
- package/workspace/guide/proactive-engine.md +21 -2
- package/workspace/lib/prepare-queue.js +76 -0
- package/workspace/lib/proactive-cli.js +57 -1
- package/workspace/lib/proactive.js +3 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +3 -3
- package/workspace/public/pm/modes-live.js +71 -2
- package/workspace/server.js +101 -1
|
@@ -3,18 +3,29 @@
|
|
|
3
3
|
# Appends ONE validated JSON line to the right pmos/state/ file. Mirrors the hosted rules:
|
|
4
4
|
# verdict enum (save_eval), pm_label enum (record_acceptance), disposition enum + the D57
|
|
5
5
|
# evidence-required-for-terminal rule (record_outcome_check). Append-only by construction — this
|
|
6
|
-
# script only ever
|
|
6
|
+
# script only ever appends; a correction is a NEW record. Parity with PMOS's own writers
|
|
7
|
+
# (pmos-cockpit-mod, D81): an acceptance needs a note and a run that exists, and a ledger that is a
|
|
8
|
+
# symlink or has another hard link is refused (the same rule as scripts/lib/sink.js, applied at the
|
|
9
|
+
# moment of the write; it does not defend against a process racing the open). No dependencies beyond POSIX + python3
|
|
7
10
|
# (used solely for safe JSON string escaping).
|
|
8
11
|
#
|
|
9
12
|
# pmos-log.sh run <run_id> <initiative_id> [human_corrections] [model]
|
|
10
13
|
# pmos-log.sh eval <run_id> <initiative_id> <score 0..1> <PASS|PASS-WITH-FINDINGS|FAIL> <rubric_path> [evaluator_model]
|
|
11
|
-
# pmos-log.sh accept <run_id> <initiative_id> <pass|fail>
|
|
14
|
+
# pmos-log.sh accept <run_id> <initiative_id> <pass|fail> <note>
|
|
12
15
|
# pmos-log.sh outcome <initiative_id> <validated|refuted|inconclusive> [evidence]
|
|
13
16
|
# pmos-log.sh friction <note...>
|
|
17
|
+
# pmos-log.sh grant <merge:<PR or branch>|publish:<artifact>> <pm> <instruction> [ttl_minutes]
|
|
18
|
+
# pmos-log.sh grant-use <grant_id> <action>
|
|
19
|
+
#
|
|
20
|
+
# `grant` and `grant-use` belong to the cockpit mod (D81). A grant is the PM's typed permission for
|
|
21
|
+
# ONE action: single use, 120 minutes unless a shorter time is given (24 hours at most), one target. A
|
|
22
|
+
# standing scope (docs-sync, outward:*) and any other scope is refused. The mod mints a grant only from
|
|
23
|
+
# the PM's typed /pmos-grant or a press, and denies a model that runs either subcommand.
|
|
24
|
+
# PMOS_STATE_DIR names the folder of the ledgers when it is not <kit>/state (PMOS's own repository).
|
|
14
25
|
set -euo pipefail
|
|
15
26
|
|
|
16
27
|
KIT="${KIT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
17
|
-
STATE="$KIT/state"
|
|
28
|
+
STATE="${PMOS_STATE_DIR:-$KIT/state}"
|
|
18
29
|
fail() { echo "pmos-log: ERROR — $*" >&2; exit 1; }
|
|
19
30
|
[ -d "$STATE" ] || fail "state dir not found at $STATE (run from a vendored kit, or set KIT_ROOT)."
|
|
20
31
|
|
|
@@ -24,8 +35,54 @@ nonempty() { # nonempty <label> <value> — ids must not be blank (hosted tools
|
|
|
24
35
|
}
|
|
25
36
|
today="$(date -u +%Y-%m-%d)"
|
|
26
37
|
|
|
38
|
+
# safe_append <file under state/> <line> — open with O_NOFOLLOW, then refuse anything that is not a
|
|
39
|
+
# regular file with a single link, and write to the descriptor that was vetted (python3 is already a
|
|
40
|
+
# dependency of this script). Nothing is written when it refuses.
|
|
41
|
+
safe_append() {
|
|
42
|
+
python3 - "$STATE/$1" "$2" <<'PY' || exit 1
|
|
43
|
+
import os, stat, sys
|
|
44
|
+
path, line = sys.argv[1], sys.argv[2]
|
|
45
|
+
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
46
|
+
try:
|
|
47
|
+
fd = os.open(path, flags, 0o644)
|
|
48
|
+
except OSError as e:
|
|
49
|
+
sys.stderr.write("pmos-log: ERROR — refusing to write %s: %s\n" % (path, e.strerror)); sys.exit(1)
|
|
50
|
+
try:
|
|
51
|
+
st = os.fstat(fd)
|
|
52
|
+
if not stat.S_ISREG(st.st_mode) or st.st_nlink != 1:
|
|
53
|
+
sys.stderr.write("pmos-log: ERROR — refusing to write %s: it is not a regular file with a single link (a symlink, a hard link or a special file).\n" % path); sys.exit(1)
|
|
54
|
+
os.write(fd, (line + "\n").encode("utf-8"))
|
|
55
|
+
finally:
|
|
56
|
+
os.close(fd)
|
|
57
|
+
PY
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# run_exists <run_id> — true when state/runs.jsonl holds a run with that id. Lines that are not JSON
|
|
61
|
+
# are skipped; the checkers report them.
|
|
62
|
+
run_exists() {
|
|
63
|
+
python3 - "$STATE/runs.jsonl" "$1" <<'PY'
|
|
64
|
+
import json, sys
|
|
65
|
+
path, rid = sys.argv[1], sys.argv[2]
|
|
66
|
+
try:
|
|
67
|
+
lines = open(path, encoding="utf-8").read().splitlines()
|
|
68
|
+
except OSError:
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
for l in lines:
|
|
71
|
+
l = l.strip()
|
|
72
|
+
if not l:
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
r = json.loads(l)
|
|
76
|
+
except ValueError:
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(r, dict) and str(r.get("run_id", "")) == rid:
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
PY
|
|
82
|
+
}
|
|
83
|
+
|
|
27
84
|
append() { # append <file> <json-line>
|
|
28
|
-
|
|
85
|
+
safe_append "$1" "$2"
|
|
29
86
|
echo "✅ appended to state/$1:"
|
|
30
87
|
echo " $2"
|
|
31
88
|
}
|
|
@@ -86,9 +143,11 @@ case "$cmd" in
|
|
|
86
143
|
append evals.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"score\":$score,\"verdict\":$(js "$4"),\"rubric_path\":$(js "$5"),\"date\":\"$today\"$emodel_field}"
|
|
87
144
|
;;
|
|
88
145
|
accept)
|
|
89
|
-
[ $# -ge
|
|
146
|
+
[ $# -ge 4 ] || fail "usage: accept <run_id> <initiative_id> <pass|fail> <note> — the note is required: an acceptance with no reasoning is the rubber stamp D12 exists to prevent."
|
|
90
147
|
nonempty run_id "$1"; nonempty initiative_id "$2"
|
|
91
148
|
case "$3" in pass|fail) ;; *) fail "pm_label must be pass | fail, got '$3'." ;; esac
|
|
149
|
+
nonempty note "$4"
|
|
150
|
+
run_exists "$1" || fail "run_id '$1' is not in state/runs.jsonl — log the run first (pmos-log.sh run ...), then accept it."
|
|
92
151
|
acc=true; [ "$3" = fail ] && acc=false
|
|
93
152
|
append acceptances.jsonl "{\"run_id\":$(js "$1"),\"initiative_id\":$(js "$2"),\"accepted\":$acc,\"pm_label\":$(js "$3"),\"note\":$(js "${4:-}"),\"date\":\"$today\"}"
|
|
94
153
|
;;
|
|
@@ -104,10 +163,63 @@ case "$cmd" in
|
|
|
104
163
|
;;
|
|
105
164
|
friction)
|
|
106
165
|
[ $# -ge 1 ] || fail "usage: friction <note...>"
|
|
107
|
-
|
|
166
|
+
safe_append discoveries.md "- $today $* #friction"
|
|
108
167
|
echo "✅ appended to state/discoveries.md: - $today $* #friction"
|
|
109
168
|
;;
|
|
169
|
+
grant)
|
|
170
|
+
[ $# -ge 3 ] || fail "usage: grant <merge:<PR or branch>|publish:<artifact>> <pm> <instruction> [ttl_minutes]"
|
|
171
|
+
nonempty pm "$2"; nonempty instruction "$3"
|
|
172
|
+
row="$(python3 - "$1" "$2" "$3" "${4:-120}" <<'PY'
|
|
173
|
+
import datetime, json, re, secrets, sys
|
|
174
|
+
scope, pm, instruction, ttl = sys.argv[1:5]
|
|
175
|
+
def die(m):
|
|
176
|
+
sys.stderr.write("pmos-log: ERROR — %s\n" % m); sys.exit(1)
|
|
177
|
+
if re.match(r"^(docs-sync|outward)(:|$)", scope):
|
|
178
|
+
die('"%s" is a standing scope. Standing grants are not allowed in this release (D81). Mint one single-use grant per action.' % scope)
|
|
179
|
+
kind, sep, target = scope.partition(":")
|
|
180
|
+
if not sep or kind not in ("merge", "publish"):
|
|
181
|
+
die('unknown scope "%s". Use merge:<PR number or branch> or publish:<artifact>.' % scope)
|
|
182
|
+
pat = r"^(?:[0-9]{1,9}|[A-Za-z0-9][A-Za-z0-9._/-]{0,79})$" if kind == "merge" else r"^[A-Za-z0-9@][A-Za-z0-9@._/-]{0,99}$"
|
|
183
|
+
if not re.match(pat, target) or "*" in target or ".." in target:
|
|
184
|
+
die('"%s" is not a single %s. A grant names one target; a wildcard is refused.' % (target, "PR number or branch name" if kind == "merge" else "artifact name"))
|
|
185
|
+
if not re.match(r"^[0-9]{1,4}$", ttl) or not (1 <= int(ttl) <= 1440):
|
|
186
|
+
die("ttl_minutes must be a whole number from 1 to 1440, got '%s'." % ttl)
|
|
187
|
+
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
|
|
188
|
+
fmt = lambda d: d.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
189
|
+
print(json.dumps({"type": "grant", "id": "g-%d-%s" % (int(now.timestamp()), secrets.token_hex(3)), "scope": kind, "target": target,
|
|
190
|
+
"pm": pm, "at": fmt(now), "expires_at": fmt(now + datetime.timedelta(minutes=int(ttl))), "uses": 1, "instruction": instruction}, separators=(",", ":")))
|
|
191
|
+
PY
|
|
192
|
+
)" || exit 1
|
|
193
|
+
append grants.jsonl "$row"
|
|
194
|
+
;;
|
|
195
|
+
grant-use)
|
|
196
|
+
[ $# -ge 2 ] || fail "usage: grant-use <grant_id> <action>"
|
|
197
|
+
nonempty grant_id "$1"; nonempty action "$2"
|
|
198
|
+
python3 - "$STATE/grants.jsonl" "$1" <<'PY' || exit 1
|
|
199
|
+
import json, sys
|
|
200
|
+
path, gid = sys.argv[1], sys.argv[2]
|
|
201
|
+
found = used = False
|
|
202
|
+
try:
|
|
203
|
+
lines = open(path, encoding="utf-8").read().splitlines()
|
|
204
|
+
except OSError:
|
|
205
|
+
lines = []
|
|
206
|
+
for l in lines:
|
|
207
|
+
try:
|
|
208
|
+
r = json.loads(l)
|
|
209
|
+
except ValueError:
|
|
210
|
+
continue
|
|
211
|
+
if isinstance(r, dict) and r.get("type") == "grant" and r.get("id") == gid:
|
|
212
|
+
found = True
|
|
213
|
+
if isinstance(r, dict) and r.get("type") == "use" and r.get("grant_id") == gid:
|
|
214
|
+
used = True
|
|
215
|
+
if not found:
|
|
216
|
+
sys.stderr.write("pmos-log: ERROR — grant_id '%s' is not in state/grants.jsonl.\n" % gid); sys.exit(1)
|
|
217
|
+
if used:
|
|
218
|
+
sys.stderr.write("pmos-log: ERROR — grant '%s' was already used. A grant is single use.\n" % gid); sys.exit(1)
|
|
219
|
+
PY
|
|
220
|
+
append grants.jsonl "{\"type\":\"use\",\"grant_id\":$(js "$1"),\"at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"action\":$(js "$2")}"
|
|
221
|
+
;;
|
|
110
222
|
*)
|
|
111
|
-
fail "unknown command '${cmd:-}' — one of: run | eval | accept | outcome | friction (see AGENTS-HUMAN.md)."
|
|
223
|
+
fail "unknown command '${cmd:-}' — one of: run | eval | accept | outcome | friction | grant | grant-use (see AGENTS-HUMAN.md)."
|
|
112
224
|
;;
|
|
113
225
|
esac
|
|
@@ -26,7 +26,7 @@ router is the bug (fix it via the weekly harness scan).
|
|
|
26
26
|
1. **Sharpness (the fog/ticket test):** *can the PM state the question or idea precisely now — not
|
|
27
27
|
answer it, state it?* If not, it is **fog**: not ready for intake. Name what would sharpen it
|
|
28
28
|
(a conversation, a datum, a spike) and stop — do not open an initiative to hold fog.
|
|
29
|
-
2. **Prior rejection:** check [planning/out-of-scope/](/README.md) for the
|
|
29
|
+
2. **Prior rejection:** check [planning/out-of-scope/](/planning/out-of-scope/README.md) for the
|
|
30
30
|
concept. If a file exists, **read it first** and surface it to the PM — the concept returns only
|
|
31
31
|
if the PM overrides the recorded reasoning, and the file is updated either way.
|
|
32
32
|
3. **The specimen test — ASK it, do not wait to be told** (D60 as amended 2026-07-24):
|
|
@@ -131,7 +131,7 @@ system, a convention set, or a prototype to validate an idea.
|
|
|
131
131
|
work has a surface, then runs inside that initiative's Build.
|
|
132
132
|
- **iterate** → back to step 5 with the PM's reaction folded in.
|
|
133
133
|
- **kill** → record one honest line on the initiative, **file the concept into
|
|
134
|
-
[planning/out-of-scope/](/README.md)** (decision, reasoning, what would
|
|
134
|
+
[planning/out-of-scope/](/planning/out-of-scope/README.md)** (decision, reasoning, what would
|
|
135
135
|
reopen it) so it is not re-prototyped from scratch, and close. A cheap kill is this lane
|
|
136
136
|
*succeeding*.
|
|
137
137
|
8. **Log.** `log_agent_run` with the disposition; friction to discoveries.md (`#friction`).
|
|
@@ -49,7 +49,7 @@ volume where informal prioritization strains) are now met.
|
|
|
49
49
|
|
|
50
50
|
- `planning/briefs/<initiative-id>-brief.md` — operational instance, never in the OKF bundle.
|
|
51
51
|
- An accepted brief becomes the PRD's parent rationale (the PRD links it). A **no-go** files the
|
|
52
|
-
concept into [planning/out-of-scope/](/README.md) so it is not re-litigated.
|
|
52
|
+
concept into [planning/out-of-scope/](/planning/out-of-scope/README.md) so it is not re-litigated.
|
|
53
53
|
|
|
54
54
|
## Level 3 sub-cases
|
|
55
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: harness-refine
|
|
3
|
-
description:
|
|
3
|
+
description: "Turn PMOS's own run evidence into concrete, evidence-cited edits to the harness. Read the eval defects, recurring friction, run transcripts, and the calibration log; cluster them into specific proposed edits \u2014 each mapped to ONE artifact (an MCP tool description, a skill body, an OKF concept, or an AGENTS.md heuristic) and citing the run/friction/eval that motivates it \u2014 including prunes of scaffolding the model now handles natively. Emit a proposals report and open a PR for the PM. Use on demand: when the PM invokes it, at the D23 quarterly harness review, or when v_friction_recurrence / recurring eval defects show a harness gap. Never merges, never self-approves, never auto-applies."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
pmos-maturity_mode: active
|
|
@@ -28,7 +28,7 @@ router is the bug (fix it via the weekly harness scan).
|
|
|
28
28
|
1. **Sharpness (the fog/ticket test):** *can the PM state the question or idea precisely now — not
|
|
29
29
|
answer it, state it?* If not, it is **fog**: not ready for intake. Name what would sharpen it
|
|
30
30
|
(a conversation, a datum, a spike) and stop — do not open an initiative to hold fog.
|
|
31
|
-
2. **Prior rejection:** check [planning/out-of-scope/](/README.md) for the
|
|
31
|
+
2. **Prior rejection:** check [planning/out-of-scope/](/planning/out-of-scope/README.md) for the
|
|
32
32
|
concept. If a file exists, **read it first** and surface it to the PM — the concept returns only
|
|
33
33
|
if the PM overrides the recorded reasoning, and the file is updated either way.
|
|
34
34
|
3. **The specimen test — ASK it, do not wait to be told** (D60 as amended 2026-07-24):
|
|
@@ -133,7 +133,7 @@ system, a convention set, or a prototype to validate an idea.
|
|
|
133
133
|
work has a surface, then runs inside that initiative's Build.
|
|
134
134
|
- **iterate** → back to step 5 with the PM's reaction folded in.
|
|
135
135
|
- **kill** → record one honest line on the initiative, **file the concept into
|
|
136
|
-
[planning/out-of-scope/](/README.md)** (decision, reasoning, what would
|
|
136
|
+
[planning/out-of-scope/](/planning/out-of-scope/README.md)** (decision, reasoning, what would
|
|
137
137
|
reopen it) so it is not re-prototyped from scratch, and close. A cheap kill is this lane
|
|
138
138
|
*succeeding*.
|
|
139
139
|
8. **Log.** `log_agent_run` with the disposition; friction to discoveries.md (`#friction`).
|
|
@@ -51,7 +51,7 @@ volume where informal prioritization strains) are now met.
|
|
|
51
51
|
|
|
52
52
|
- `planning/briefs/<initiative-id>-brief.md` — operational instance, never in the OKF bundle.
|
|
53
53
|
- An accepted brief becomes the PRD's parent rationale (the PRD links it). A **no-go** files the
|
|
54
|
-
concept into [planning/out-of-scope/](/README.md) so it is not re-litigated.
|
|
54
|
+
concept into [planning/out-of-scope/](/planning/out-of-scope/README.md) so it is not re-litigated.
|
|
55
55
|
|
|
56
56
|
## Level 3 sub-cases
|
|
57
57
|
|
|
@@ -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-04T08:22:32Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/README.md
CHANGED
|
@@ -86,6 +86,10 @@ The top of Today is a brief: a greeting, the day's calendar, ranked priorities a
|
|
|
86
86
|
|
|
87
87
|
`pmos kg find|card|neighbors|section|why|stats` builds a map of the repository's tracked Markdown files from relations they already write down (links, frontmatter paths, `D<nn>` citations, `initiative_id`, `parent_kr`) and answers from it, so an agent reads one card or one section and not a 130 KB file. `lib/kg.js` is pure; `lib/kg-cli.js` reads the files, keeps the map in the runtime state directory (rebuilt when HEAD or an uncommitted `.md` changes) and writes nothing in the repository. A link to a missing file is listed as dangling and is never an edge; `lib/kg-dangling-rules.json` says which dangling links are history by design. It does not guess, use embeddings, or call a model or the network. `node scripts/kg-bench.js` replays two written strategies over the fixed questions in `planning/evals/knowledge-graph-questions.md`; it is a simulation, and says "NOT REPORTABLE" until three of the questions are the PM's. Guide page: `guide/knowledge-graph.md`. Tests: `kg.test.js`.
|
|
88
88
|
|
|
89
|
+
## Background prepare (`proactive-engine`, slice D3)
|
|
90
|
+
|
|
91
|
+
From a Today item that names an initiative the PM can approve **one plan hash**: the step an agent may do now, with the exact first input. The server queues that approval (`prepare-queue.jsonl` in the runtime state directory) and a 60-second tick starts it through the same `startWork` path as the C1 "go" button — origin `next-step`, the same propose-only rules, the same session budget — only when the switch is on (default off), it is not quiet hours, the budget allows, and the plan hash has not changed. An approval expires after 24 hours, at most 5 can be queued, and a changed plan is skipped, never run. `lib/prepare-queue.js` holds the pure rules (`fold`, `checkRequest`, `canQueue`, `decide`); the routes are `/api/proactive/prepare/{plan,queue,cancel,run}` and `GET /api/proactive/prepare`; `pmos proactive prepare plan|queue|status|cancel|run` is the terminal face. "Run now" ignores quiet hours only. The C1 run record carries `via: 'background'`. Guide page: `guide/proactive-engine.md`. Tests: `proactive-prepare.test.js` (server, CLI, boundary) and `pm-r25-test.js` (browser). Not verified here: a real `claude` session doing the work; the suites use a stand-in agent that records what it was typed.
|
|
92
|
+
|
|
89
93
|
## Scheduled tasks from the terminal (`schedule-cli`)
|
|
90
94
|
|
|
91
95
|
`pmos schedule` and `pmos skill` talk to the running workspace server, so the terminal and the workspace share one schedule store and one validator. Every change is saved the moment the command succeeds. A refused value names what is allowed and changes nothing. With no server running the command says so and exits 1.
|
|
@@ -22,6 +22,23 @@ The top of Today is the morning brief: a greeting, the day's calendar, the prior
|
|
|
22
22
|
- A brief is refused, with the reason and with nothing stored, when it has a wrong shape, an unknown source, a confidence outside 0 to 1, more than 12 items or 20 events, or a text field that is too long.
|
|
23
23
|
- The brief is stored in the workspace state folder on this computer. It is not committed.
|
|
24
24
|
|
|
25
|
+
## Background preparation
|
|
26
|
+
|
|
27
|
+
From a Today item that names an initiative, you can ask for the plan and approve it. The workspace then starts that session itself, later. It uses the same path as the next-step go button, so the plan, the prompt, the propose-only rules and the session budget are the same.
|
|
28
|
+
|
|
29
|
+
- Press **prepare in the background** on an item. Today shows the step an agent session may work on now, where the work would happen, the budget and the exact first input. Press **queue it** to approve that plan.
|
|
30
|
+
- Background prepare is off by default. Turn it on in the settings. When it is off, nothing can be queued and nothing queued is started. A queued entry waits while it is off. It is not dropped.
|
|
31
|
+
- The approval is for one plan. If the plan has changed when the workspace is ready to start, the entry is skipped and the reason is shown. Queue it again to approve the new plan.
|
|
32
|
+
- An approval expires after 24 hours. An expired entry is never started.
|
|
33
|
+
- The workspace starts an entry only when the switch is on, it is not quiet hours, the session budget allows it, and the plan is the one you approved. If the budget is full, the entry waits. Entries start oldest first. At most 5 entries wait at once.
|
|
34
|
+
- **run the queue now** looks at the queue immediately. It ignores quiet hours and nothing else.
|
|
35
|
+
- A queued entry can be cancelled. A cancelled entry is never started.
|
|
36
|
+
- The session starts with no profile, so it names no outside source, and with typed scoring off. It commits only to a local initiative branch or writes drafts. It does not push, open a pull request, send, post, accept, approve a contract or merge. The contract, acceptance and open items are your steps and cannot be queued.
|
|
37
|
+
- A started session counts in the same session budget as a next-step go. Switch to it from Today, and review it from the initiative as for any prepared step.
|
|
38
|
+
- It runs only while the workspace server runs. The queue is stored in the workspace state folder on this computer.
|
|
39
|
+
|
|
40
|
+
The states of an entry are queued, started, skipped, expired and cancelled.
|
|
41
|
+
|
|
25
42
|
## What it shows
|
|
26
43
|
|
|
27
44
|
Today scans for five conditions. Each one becomes one item with the reason and the evidence.
|
|
@@ -52,6 +69,7 @@ A verdict never changes a status, a lifecycle record or an acceptance. In this r
|
|
|
52
69
|
| engine on | Off stops automatic scans. The scan now button still works. |
|
|
53
70
|
| watch: needs, returned, review, schedule, stalled | Turn one trigger off. |
|
|
54
71
|
| brief sources | One switch for each of calendar, wispr, mail, jira and confluence. |
|
|
72
|
+
| background prepare | Off by default. On lets the workspace start a session for a plan you approved. |
|
|
55
73
|
| typed scoring | Off by default. On lets the skill send titles and metadata to the Typesafe service. |
|
|
56
74
|
| name | Used in the greeting. |
|
|
57
75
|
| stalled after N days | The stalled threshold, from 1 to 90 days. |
|
|
@@ -62,14 +80,15 @@ A verdict never changes a status, a lifecycle record or an acceptance. In this r
|
|
|
62
80
|
|
|
63
81
|
- `pmos proactive today` lists the items.
|
|
64
82
|
- `pmos proactive scan` scans now.
|
|
65
|
-
- `pmos proactive settings` shows the settings. Add `--enabled`, `--trigger NAME=on|off`, `--stalled-days`, `--quiet HH:MM-HH:MM|none`, `--pause-learning`, `--brief-source NAME=on|off`, `--brief-scoring on|off` or `--name` to change one.
|
|
83
|
+
- `pmos proactive settings` shows the settings. Add `--enabled`, `--trigger NAME=on|off`, `--stalled-days`, `--quiet HH:MM-HH:MM|none`, `--pause-learning`, `--background-prepare on|off`, `--brief-source NAME=on|off`, `--brief-scoring on|off` or `--name` to change one.
|
|
66
84
|
- `pmos proactive brief` shows today's brief. `pmos proactive brief submit --file PATH` submits one. `pmos proactive brief schedule` makes the 06:45 schedule for the current repository.
|
|
85
|
+
- `pmos proactive prepare` shows the queue. `pmos proactive prepare plan --initiative ID` shows a plan and its hash. `pmos proactive prepare queue --initiative ID --item ITEM --sha HASH` approves it. `pmos proactive prepare cancel ID` cancels an entry. `pmos proactive prepare run` looks at the queue now.
|
|
67
86
|
- `pmos proactive feedback <id> useful|not-useful|wrong-priority|opened|dismissed` records feedback. Use `--missed "text"` for a missed item.
|
|
68
87
|
|
|
69
88
|
## Limits
|
|
70
89
|
|
|
71
90
|
- It runs only while the workspace server runs. It does not catch up for time the server was off. A sleeping computer gets no scan.
|
|
72
91
|
- The items from the records come from this computer. The workspace itself does not read your mail or your calendar. Only the morning-brief session does, and only the sources you switched on.
|
|
73
|
-
- It does not start a session, push, send, post, accept or merge. It only shows items and records your feedback.
|
|
92
|
+
- It does not start a session, push, send, post, accept or merge. It only shows items and records your feedback. The one exception is background preparation, which starts a session only for a plan you approved, as described above.
|
|
74
93
|
- An unreadable repository is listed as not read. An empty list is not proof that nothing waits there.
|
|
75
94
|
- The records stay in the workspace state folder on this computer. They are not committed.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* prepare-queue.js — the rules for background preparation: what may be queued, and when a queued entry may start.
|
|
3
|
+
*
|
|
4
|
+
* Pure: no I/O, no clock, no model. The server passes what is true now (the settings, the entries read from the ledger, a
|
|
5
|
+
* fresh prepare of the same step, the session budget) and this module decides. An approval is ONE plan hash, never a
|
|
6
|
+
* standing permission: a plan that changed is skipped, an approval older than 24 hours expires, and nothing is caught up.
|
|
7
|
+
*
|
|
8
|
+
* fold(events) → the entries, each with its latest status
|
|
9
|
+
* checkRequest(body) → the shape of a queue request, or why it is refused
|
|
10
|
+
* canQueue({settings, entries, req}) → null, or the reason a request is refused
|
|
11
|
+
* decide({entry, now, settings, quiet, manual, fresh, budget}) → {action, reason}
|
|
12
|
+
*
|
|
13
|
+
* Statuses: queued · started · skipped · expired · cancelled. Only `queued` can change; the other four are final.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_QUEUED = 5;
|
|
16
|
+
const EXPIRE_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const FINAL = ['started', 'skipped', 'expired', 'cancelled'];
|
|
18
|
+
const HASH = /^[0-9a-f]{16}$/;
|
|
19
|
+
const ID = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
20
|
+
const ITEM = /^[\w.:-]{1,120}$/;
|
|
21
|
+
const CAND = /^[0-9a-f]{12}$/;
|
|
22
|
+
|
|
23
|
+
/** The latest status of every entry, oldest first. A line that names no entry is ignored. */
|
|
24
|
+
function fold(events) {
|
|
25
|
+
const by = new Map();
|
|
26
|
+
for (const e of Array.isArray(events) ? events : []) {
|
|
27
|
+
if (!e || typeof e.id !== 'string') continue;
|
|
28
|
+
if (e.event === 'queued') { if (!by.has(e.id)) by.set(e.id, { id: e.id, status: 'queued', queued_at: e.at, ws: e.ws, initiative_id: e.initiative_id, item_id: e.item_id, key: e.key, candidate_id: e.candidate_id || null, prompt_sha: e.prompt_sha, branch: e.branch || null, reason: null, session_id: null, at: e.at }); continue; }
|
|
29
|
+
const cur = by.get(e.id);
|
|
30
|
+
if (!cur || FINAL.includes(cur.status) || !FINAL.includes(e.event)) continue;
|
|
31
|
+
cur.status = e.event; cur.reason = e.reason || null; cur.session_id = e.session_id || null; cur.at = e.at;
|
|
32
|
+
}
|
|
33
|
+
return [...by.values()];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @returns {{ok:true, req:object}|{ok:false, error:string}} */
|
|
37
|
+
function checkRequest(body) {
|
|
38
|
+
const b = body && typeof body === 'object' ? body : {};
|
|
39
|
+
if (b.confirm !== true) return { ok: false, error: 'confirm is required: this lets the workspace start a session later' };
|
|
40
|
+
if (typeof b.ws !== 'string' || !b.ws) return { ok: false, error: 'ws is required' };
|
|
41
|
+
if (typeof b.initiative_id !== 'string' || !ID.test(b.initiative_id)) return { ok: false, error: 'initiative_id is required' };
|
|
42
|
+
if (typeof b.item_id !== 'string' || !ITEM.test(b.item_id)) return { ok: false, error: 'item_id is required' };
|
|
43
|
+
if (typeof b.prompt_sha !== 'string' || !HASH.test(b.prompt_sha)) return { ok: false, error: 'prompt_sha is required: queue the plan you were shown' };
|
|
44
|
+
if (b.candidate_id !== undefined && b.candidate_id !== null && b.candidate_id !== '' && !CAND.test(String(b.candidate_id))) return { ok: false, error: 'candidate_id is not a candidate id' };
|
|
45
|
+
return { ok: true, req: { ws: b.ws, initiative_id: b.initiative_id, item_id: b.item_id, prompt_sha: b.prompt_sha, candidate_id: b.candidate_id || null } };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @returns {string|null} the reason a request is refused, or null */
|
|
49
|
+
function canQueue({ settings, entries, req }) {
|
|
50
|
+
if (!settings || settings.backgroundPrepare !== true) return 'background prepare is off. Turn it on in the Today settings first.';
|
|
51
|
+
const queued = (entries || []).filter((e) => e.status === 'queued');
|
|
52
|
+
if (queued.some((e) => e.ws === req.ws && e.initiative_id === req.initiative_id && e.item_id === req.item_id)) return 'this step is already queued';
|
|
53
|
+
if (queued.length >= MAX_QUEUED) return `${queued.length} entries are already waiting (the limit is ${MAX_QUEUED}). Cancel one or wait for it to start.`;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What the runner does with one queued entry right now.
|
|
59
|
+
* @param {{entry:object, now:number, settings:object, quiet:boolean, manual:boolean, fresh:object, budget:object}} s
|
|
60
|
+
* @returns {{action:'start'|'wait'|'skip'|'expire', reason:string|null}}
|
|
61
|
+
*/
|
|
62
|
+
function decide(s) {
|
|
63
|
+
const e = s.entry;
|
|
64
|
+
const age = s.now - Date.parse(e.queued_at);
|
|
65
|
+
if (!Number.isFinite(age) || age > EXPIRE_MS) return { action: 'expire', reason: 'the approval is older than 24 hours — queue it again if you still want it' };
|
|
66
|
+
if (!s.settings || s.settings.backgroundPrepare !== true) return { action: 'wait', reason: 'background prepare is off' };
|
|
67
|
+
if (s.quiet && !s.manual) return { action: 'wait', reason: 'quiet hours' };
|
|
68
|
+
const f = s.fresh || {};
|
|
69
|
+
if (!f.ok) return { action: 'skip', reason: f.error || 'the step could not be prepared again' };
|
|
70
|
+
if (f.refused) return { action: 'skip', reason: f.refused.why };
|
|
71
|
+
if (f.prompt_sha !== e.prompt_sha) return { action: 'skip', reason: 'the plan changed since you approved it — queue it again to approve the new one' };
|
|
72
|
+
if (!s.budget || !s.budget.allowed) return { action: 'wait', reason: (s.budget && s.budget.reason) || 'no session budget' };
|
|
73
|
+
return { action: 'start', reason: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { MAX_QUEUED, EXPIRE_MS, FINAL, fold, checkRequest, canQueue, decide };
|
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
* pmos proactive brief [show]
|
|
14
14
|
* pmos proactive brief submit --file PATH
|
|
15
15
|
* pmos proactive brief schedule [--repo PATH]
|
|
16
|
+
* pmos proactive prepare plan --initiative ID [--repo PATH]
|
|
17
|
+
* pmos proactive prepare queue --initiative ID --item ITEM --sha HASH [--repo PATH]
|
|
18
|
+
* pmos proactive prepare status | cancel ID | run
|
|
16
19
|
*/
|
|
17
20
|
const fs = require('fs');
|
|
18
21
|
const path = require('path');
|
|
19
22
|
const { execFileSync } = require('child_process');
|
|
20
23
|
const { parseArgs } = require('./schedule-cli');
|
|
21
24
|
|
|
22
|
-
const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief'] };
|
|
25
|
+
const COMMANDS = { proactive: ['today', 'scan', 'settings', 'feedback', 'brief', 'prepare'] };
|
|
23
26
|
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'jira', 'confluence'];
|
|
24
27
|
const onOff = (v, what) => (v === 'on' || v === 'true' ? true : v === 'off' || v === 'false' ? false : { err: `${what} takes on or off` });
|
|
25
28
|
|
|
@@ -63,6 +66,7 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
63
66
|
patch.triggers = { [m[1]]: m[2] === 'on' };
|
|
64
67
|
}
|
|
65
68
|
if ('name' in flags) patch.name = flags.name;
|
|
69
|
+
if ('background-prepare' in flags) { const v = onOff(flags['background-prepare'], '--background-prepare'); if (v.err) return fail(v.err); patch.backgroundPrepare = v; }
|
|
66
70
|
if ('brief-scoring' in flags) { const v = onOff(flags['brief-scoring'], '--brief-scoring'); if (v.err) return fail(v.err); patch.briefScoring = v; }
|
|
67
71
|
if ('brief-source' in flags) {
|
|
68
72
|
const m = /^([a-z]+)=(on|off)$/.exec(flags['brief-source']);
|
|
@@ -82,9 +86,61 @@ async function run(argv, { call, say, cwd = process.cwd() }) {
|
|
|
82
86
|
}
|
|
83
87
|
say(`engine ${s.enabled ? 'on' : 'off'} · stalled after ${s.stalledDays} days · quiet ${s.quiet ? s.quiet.start + '-' + s.quiet.end : 'none'} · learning ${s.pauseLearning ? 'paused' : 'on'}`);
|
|
84
88
|
say('brief sources: ' + BRIEF_SOURCES.map((k) => `${k} ${s.briefSources && s.briefSources[k] ? 'on' : 'off'}`).join(', ') + ' · scoring ' + (s.briefScoring ? 'on' : 'off') + (s.name ? ' · name ' + s.name : ''));
|
|
89
|
+
say('background prepare ' + (s.backgroundPrepare ? 'on' : 'off'));
|
|
85
90
|
say('watching: ' + Object.entries(s.triggers).map(([k, v]) => `${k} ${v ? 'on' : 'off'}`).join(', '));
|
|
86
91
|
return 0;
|
|
87
92
|
}
|
|
93
|
+
if (verb === 'prepare') {
|
|
94
|
+
const sub = pos[0] || 'status';
|
|
95
|
+
const wsOf = async () => {
|
|
96
|
+
let root; try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: flags.repo ? path.resolve(cwd, flags.repo) : cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return { err: 'this is not inside a git repository — use --repo PATH' }; }
|
|
97
|
+
const wl = await call('GET', '/api/workspaces');
|
|
98
|
+
const hit = ((wl && wl.workspaces) || []).find((w) => path.resolve(w.root) === path.resolve(root));
|
|
99
|
+
return hit ? { ws: hit.id } : { err: `${root} is not a registered workspace — open it with pmos first` };
|
|
100
|
+
};
|
|
101
|
+
if (sub === 'plan' || sub === 'queue') {
|
|
102
|
+
if (!flags.initiative) return fail(`usage: pmos proactive prepare ${sub} --initiative ID${sub === 'queue' ? ' --item ITEM --sha HASH' : ''}`);
|
|
103
|
+
const w = await wsOf(); if (w.err) return fail(w.err);
|
|
104
|
+
if (sub === 'plan') {
|
|
105
|
+
const r = await call('POST', '/api/proactive/prepare/plan', { ws: w.ws, initiative_id: flags.initiative });
|
|
106
|
+
if (!r || r.ok === false) return fail((r && r.error) || 'the server did not answer');
|
|
107
|
+
say(`step: ${r.item.key} — ${r.item.text} (item ${r.item.id})`);
|
|
108
|
+
if (r.refused) { say('refused: ' + r.refused.why); return 1; }
|
|
109
|
+
say(r.plan.why || r.plan.plan);
|
|
110
|
+
say(`budget: ${r.budget.running} of ${r.budget.limits.concurrent} running · ${r.budget.startedToday} of ${r.budget.limits.perDay} started today`);
|
|
111
|
+
say('background prepare is ' + (r.background.enabled ? 'on' : 'off. Turn it on with: pmos proactive settings --background-prepare on'));
|
|
112
|
+
say('plan ' + r.prompt_sha + ' — the exact first input:');
|
|
113
|
+
say(r.prompt);
|
|
114
|
+
say(`queue it with: pmos proactive prepare queue --initiative ${r.initiative_id} --item ${r.item.id} --sha ${r.prompt_sha}`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
if (!flags.item || !flags.sha) return fail('usage: pmos proactive prepare queue --initiative ID --item ITEM --sha HASH');
|
|
118
|
+
const r = await call('POST', '/api/proactive/prepare/queue', { ws: w.ws, initiative_id: flags.initiative, item_id: flags.item, prompt_sha: flags.sha, confirm: true });
|
|
119
|
+
if (!r || r.ok === false) return fail(`refused — ${(r && r.error) || 'the server did not answer'}. Nothing was queued.`);
|
|
120
|
+
say(`queued ${r.entry.id} — the workspace starts it when the budget allows. It expires in 24 hours.`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (sub === 'cancel') {
|
|
124
|
+
if (!pos[1]) return fail('usage: pmos proactive prepare cancel ID');
|
|
125
|
+
const r = await call('POST', '/api/proactive/prepare/cancel', { id: pos[1] });
|
|
126
|
+
if (!r || r.ok === false) return fail((r && r.error) || 'the server did not answer');
|
|
127
|
+
say('cancelled'); return 0;
|
|
128
|
+
}
|
|
129
|
+
if (sub === 'run') {
|
|
130
|
+
const r = await call('POST', '/api/proactive/prepare/run', {});
|
|
131
|
+
if (!r || r.ok === false) return fail((r && r.error) || 'the server did not answer');
|
|
132
|
+
if (!r.results.length) say('nothing is queued');
|
|
133
|
+
for (const x of r.results) say(`${x.id} ${x.action}${x.reason ? ' — ' + x.reason : ''}${x.session_id ? ' (session ' + x.session_id + ')' : ''}`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
if (sub !== 'status') return fail('usage: pmos proactive prepare [status|plan|queue|cancel ID|run]');
|
|
137
|
+
const r = await call('GET', '/api/proactive/prepare');
|
|
138
|
+
if (!r || r.ok === false) return fail((r && r.error) || 'the server did not answer');
|
|
139
|
+
say(`background prepare ${r.enabled ? 'on' : 'off'}${r.quiet ? ' · quiet hours now' : ''} · ${r.budget.running} of ${r.budget.limits.concurrent} running · ${r.budget.startedToday} of ${r.budget.limits.perDay} started today`);
|
|
140
|
+
if (!r.entries.length) say('nothing has been queued');
|
|
141
|
+
for (const e of r.entries) say(`${e.id} ${e.status} ${e.label}${e.reason ? ' — ' + e.reason : ''}`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
88
144
|
if (verb === 'brief') {
|
|
89
145
|
const sub = pos[0] || 'show';
|
|
90
146
|
if (sub === 'submit') {
|
|
@@ -32,7 +32,7 @@ const OWNER = { needs: 'pm', returned: 'pm', review: 'pm', schedule: 'pm', stall
|
|
|
32
32
|
const BRIEF_SOURCES = ['calendar', 'wispr', 'mail', 'jira', 'confluence'];
|
|
33
33
|
// Mail is off by default: a brief reads titles and metadata of the other sources, and the PM turns mail on.
|
|
34
34
|
const BRIEF_DEFAULT = Object.freeze({ calendar: true, wispr: true, mail: false, jira: true, confluence: true });
|
|
35
|
-
const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false });
|
|
35
|
+
const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, backgroundPrepare: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false });
|
|
36
36
|
|
|
37
37
|
const idOf = (key) => crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 12);
|
|
38
38
|
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
@@ -161,10 +161,11 @@ const HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
|
161
161
|
/** @returns {{ok:true, settings:object}|{ok:false, error:string}} — `raw` is merged over the defaults; nothing is partly applied. */
|
|
162
162
|
function normalizeSettings(raw) {
|
|
163
163
|
const r = raw && typeof raw === 'object' ? raw : {};
|
|
164
|
-
const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning };
|
|
164
|
+
const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning };
|
|
165
165
|
if (r.enabled !== undefined) { if (typeof r.enabled !== 'boolean') return { ok: false, error: 'enabled must be true or false' }; s.enabled = r.enabled; }
|
|
166
166
|
if (r.name !== undefined) { if (typeof r.name !== 'string' || r.name.length > 40) return { ok: false, error: 'name must be text of at most 40 characters' }; s.name = r.name.replace(/\s+/g, ' ').trim(); }
|
|
167
167
|
if (r.briefScoring !== undefined) { if (typeof r.briefScoring !== 'boolean') return { ok: false, error: 'briefScoring must be true or false' }; s.briefScoring = r.briefScoring; }
|
|
168
|
+
if (r.backgroundPrepare !== undefined) { if (typeof r.backgroundPrepare !== 'boolean') return { ok: false, error: 'backgroundPrepare must be true or false' }; s.backgroundPrepare = r.backgroundPrepare; }
|
|
168
169
|
if (r.briefSources !== undefined) {
|
|
169
170
|
if (!r.briefSources || typeof r.briefSources !== 'object' || Array.isArray(r.briefSources)) return { ok: false, error: `briefSources must be an object of ${BRIEF_SOURCES.join(', ')} → true or false` };
|
|
170
171
|
for (const [k, v] of Object.entries(r.briefSources)) {
|
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
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",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
12
12
|
"test": "node test.js && npm run test:modules",
|
|
13
|
-
"test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node jev-claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js && node kg.test.js",
|
|
13
|
+
"test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node jev-claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js && node kg.test.js && node proactive-prepare.test.js",
|
|
14
14
|
"test:ui": "node ui-test.js",
|
|
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 && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js && node pm-r24-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 && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js && node pm-r24-test.js && node pm-r25-test.js",
|
|
19
19
|
"test:offline": "node offline-tarball.test.js"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|