@stravica-ai/digital-operator-pa 0.1.20 → 0.1.21

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.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,12 +1,23 @@
1
1
  // work-cli.mjs — argv router for `pa work <verb>`.
2
2
  //
3
3
  // Verbs:
4
- // pa work create [--project <id>] --title "..." --category <c> [--priority N] [--summary "..."] [--owner ...] [--assignee ...]
4
+ // pa work create [--project <id>] --title "..." --category <c> [--priority N] [--summary "..."] [--owner ...] [--assignee ...] [--author <token>]
5
5
  // pa work status <id> <status>
6
6
  // pa work list [--project <id>] [--status ...] [--mine] [--theirs] [--all] [--json]
7
7
  //
8
8
  // PA name (for --mine resolution) is read from
9
9
  // operator/config/preferences.json. Falls back to 'pa' if missing.
10
+ //
11
+ // The `paName` field ALSO doubles as the fallback author-token source
12
+ // for `pa work create` (writer-scoped id allocation, R1 of the
13
+ // 2026-08-11 library remediation train). Precedence, highest first:
14
+ // 1. --author flag (or opts.author from a programmatic caller)
15
+ // 2. PA_AUTHOR_TOKEN env var
16
+ // 3. preferences.json `paName`
17
+ // If none resolve, `pa work create` hard-errors — the whole point of
18
+ // R1 is that a mint with no author identity is a defect, not a
19
+ // fall-back-to-default case. Full rationale + tests in
20
+ // `@stravica-ai/engineering-pa-core/src/workItemId/index.mjs`.
10
21
 
11
22
  import { readPreferences } from '../../../engineering-pa-core/src/preferences.mjs';
12
23
 
@@ -18,6 +29,7 @@ const USAGE = `pa work — manage operator-repo work items
18
29
  Usage:
19
30
  pa work create [--project <id>] --title "..." --category <feature|bug|chore|discussion|research>
20
31
  [--priority 0|1|2|3] [--summary "..."] [--owner ...] [--assignee ...]
32
+ [--author <token>]
21
33
  pa work status <id> <draft|planned|in-progress|blocked|done|archived>
22
34
  pa work list [--project <id>] [--status ...] [--category ...] [--mine] [--theirs] [--all] [--json]
23
35
  pa work -h, --help
@@ -26,6 +38,9 @@ Notes:
26
38
  - status=done stamps \`completedAt\` to today (only if not already set).
27
39
  - --mine resolves the PA name from operator/config/preferences.json
28
40
  (paName / agentName). Default: "pa".
41
+ - --author sets the writer-scoped id's author token. Precedence:
42
+ --author > PA_AUTHOR_TOKEN env > preferences.json paName. Must
43
+ match /^[a-z]{2,12}$/ and be present in operator/config/authors.json.
29
44
 
30
45
  Exit codes:
31
46
  0 ok | 1 usage/not-found | 2 operational failure
@@ -89,11 +104,29 @@ async function getPaName() {
89
104
  }
90
105
  }
91
106
 
107
+ /**
108
+ * Read the raw `paName` (device-local overlay merged over shared). Unlike
109
+ * `getPaName`, this returns null if the field is unset — createWork's
110
+ * author-resolver treats null as "skip this source", not "fall back to
111
+ * literal 'pa'" (which would silently mask a missing token).
112
+ */
113
+ async function readPreferencesPaName() {
114
+ try {
115
+ const repoRoot = await getRepoRoot();
116
+ const config = await readPreferences(repoRoot);
117
+ if (!config) return null;
118
+ return config.paName || config.agentName || null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
92
124
  async function doCreate(args) {
93
125
  const { flags } = parseFlags(args);
94
126
  const priority = flags.priority != null && flags.priority !== true
95
127
  ? Number(flags.priority)
96
128
  : null;
129
+ const preferencesPaName = await readPreferencesPaName();
97
130
  const res = await createWork({
98
131
  projectId: typeof flags.project === 'string' ? flags.project : null,
99
132
  title: typeof flags.title === 'string' ? flags.title : null,
@@ -102,6 +135,8 @@ async function doCreate(args) {
102
135
  summary: typeof flags.summary === 'string' ? flags.summary : null,
103
136
  owner: typeof flags.owner === 'string' ? flags.owner : null,
104
137
  assignee: typeof flags.assignee === 'string' ? flags.assignee : null,
138
+ author: typeof flags.author === 'string' ? flags.author : null,
139
+ preferencesPaName,
105
140
  });
106
141
  process.stdout.write(`created ${res.id}\n`);
107
142
  process.stdout.write(` ${res.path}\n`);
@@ -1,18 +1,30 @@
1
1
  // work.mjs — write-side commands for `pa work <verb>`.
2
2
  //
3
- // Source of truth is work/w-YYYY-MM-DD-NNN.json. We never cache.
3
+ // Source of truth is work/w-YYYY-MM-DD-*.json. We never cache.
4
4
  //
5
5
  // What lives here:
6
- // - create: scaffold a new work item with the next-free NNN for today
6
+ // - create: scaffold a new work item with the next-free NNN in the
7
+ // CURRENT WRITER'S per-day sequence
7
8
  // - status: change status (stamps `completedAt` on done)
8
9
  // - list: list work items, optionally filtered
9
10
  //
10
- // ID format and status/category enums match
11
- // packages/core/schema/work-item.json.
11
+ // Id format, parsing, and author resolution all live in
12
+ // `@stravica-ai/engineering-pa-core/src/workItemId/index.mjs` — the
13
+ // single-source primitive for the whole estate. See its header for the
14
+ // two legal forms (legacy + writer-scoped), the R1 remediation
15
+ // background, and the author-token precedence rules.
12
16
 
13
17
  import { existsSync } from 'node:fs';
14
18
  import { join } from 'node:path';
15
19
 
20
+ import {
21
+ parseWorkItemId,
22
+ isValidWorkItemId,
23
+ buildWorkItemId,
24
+ resolveAuthorToken,
25
+ maxNForAuthorOnDate,
26
+ } from '../../../engineering-pa-core/src/workItemId/index.mjs';
27
+
16
28
  import { getRepoRoot } from './repo-root.mjs';
17
29
  import { readJson, writeJson, ensureDir, listJsonFiles, today } from './io.mjs';
18
30
 
@@ -20,8 +32,6 @@ const STATUSES = new Set(['draft', 'planned', 'in-progress', 'blocked', 'done',
20
32
 
21
33
  const CATEGORIES = new Set(['feature', 'bug', 'chore', 'discussion', 'research']);
22
34
 
23
- const ID_RE = /^w-(\d{4}-\d{2}-\d{2})-(\d{3})$/;
24
-
25
35
  function validateStatus(status) {
26
36
  if (!STATUSES.has(status)) {
27
37
  const err = new Error(
@@ -53,38 +63,44 @@ function validatePriority(priority) {
53
63
  }
54
64
 
55
65
  /**
56
- * Scan work/*.json for the highest NNN with today's date. Returns the
57
- * next free NNN, zero-padded to 3 digits. Result is always "001"–"999".
66
+ * Highest NNN already allocated for `dateStr` in THIS author's per-day
67
+ * sequence. Legacy items (`w-YYYY-MM-DD-NNN` with no author segment)
68
+ * do not count toward any author's sequence — each writer starts from
69
+ * 001 on the day writer-scoping takes effect and existing legacy items
70
+ * simply occupy a separate, older space.
58
71
  *
59
72
  * If 999 is already taken, throws — callers should treat as an
60
- * operational failure and report. In practice this is unreachable.
73
+ * operational failure and report. In practice this is unreachable
74
+ * because NNN is per-author-per-day, not shared.
61
75
  */
62
- async function nextWorkNumber(repoRoot, dateStr) {
76
+ async function nextWorkNumber(repoRoot, dateStr, author) {
63
77
  const dir = join(repoRoot, 'work');
64
78
  const files = await listJsonFiles(dir);
65
- let max = 0;
66
- const prefix = `w-${dateStr}-`;
67
- for (const file of files) {
68
- if (!file.startsWith(prefix)) continue;
69
- const m = file.match(/^w-\d{4}-\d{2}-\d{2}-(\d{3})\.json$/);
70
- if (!m) continue;
71
- const n = parseInt(m[1], 10);
72
- if (Number.isFinite(n) && n > max) max = n;
73
- }
79
+ const max = maxNForAuthorOnDate(files, dateStr, author);
74
80
  const next = max + 1;
75
81
  if (next > 999) {
76
- const err = new Error(`no free work-item slot for ${dateStr} (NNN exhausted)`);
82
+ const err = new Error(
83
+ `no free work-item slot for ${dateStr} in author=${author} sequence (NNN exhausted)`,
84
+ );
77
85
  err.exitCode = 2;
78
86
  throw err;
79
87
  }
80
- return String(next).padStart(3, '0');
88
+ return next;
81
89
  }
82
90
 
83
91
  /**
84
92
  * Create a new work item. Returns { id, path }.
85
93
  *
86
94
  * Status defaults to "draft" per the brief. Date and NNN are
87
- * auto-derived from today; we do not accept overrides.
95
+ * auto-derived; we do not accept overrides.
96
+ *
97
+ * Author token comes from `opts.author` (explicit; wins over anything
98
+ * else), the `PA_AUTHOR_TOKEN` env var, or the `paName` field of
99
+ * `operator/config/preferences.json` (passed in as
100
+ * `opts.preferencesPaName` by the CLI entry). If none resolve, throws
101
+ * with an actionable message naming all three sources — a mint with no
102
+ * author identity is the exact defect R1 exists to close, so we refuse
103
+ * to fall back to any hard-coded default.
88
104
  */
89
105
  export async function createWork(opts) {
90
106
  if (!opts.title) {
@@ -95,13 +111,24 @@ export async function createWork(opts) {
95
111
  validateCategory(opts.category);
96
112
  validatePriority(opts.priority);
97
113
 
114
+ let author;
115
+ try {
116
+ author = resolveAuthorToken({
117
+ explicit: opts.author,
118
+ preferencesPaName: opts.preferencesPaName,
119
+ });
120
+ } catch (e) {
121
+ e.exitCode = 1;
122
+ throw e;
123
+ }
124
+
98
125
  const repoRoot = await getRepoRoot();
99
126
  const workDir = join(repoRoot, 'work');
100
127
  await ensureDir(workDir);
101
128
 
102
129
  const dateStr = today();
103
- const nnn = await nextWorkNumber(repoRoot, dateStr);
104
- const id = `w-${dateStr}-${nnn}`;
130
+ const nnn = await nextWorkNumber(repoRoot, dateStr, author);
131
+ const id = buildWorkItemId({ date: dateStr, author, nnn });
105
132
  const path = join(workDir, `${id}.json`);
106
133
 
107
134
  const item = {
@@ -127,10 +154,16 @@ export async function createWork(opts) {
127
154
  /**
128
155
  * Set work-item status. Stamps `completedAt` to today's date when
129
156
  * status=done (only if not already set). Always bumps `updated`.
157
+ *
158
+ * Accepts both legacy `w-YYYY-MM-DD-NNN` and writer-scoped
159
+ * `w-YYYY-MM-DD-<author>-NNN` ids — every existing item in the estate
160
+ * predates R1 and must remain mutable through this command.
130
161
  */
131
162
  export async function setWorkStatus(id, status) {
132
- if (!ID_RE.test(id)) {
133
- const err = new Error(`invalid work id: ${id} (expected w-YYYY-MM-DD-NNN)`);
163
+ if (!isValidWorkItemId(id)) {
164
+ const err = new Error(
165
+ `invalid work id: ${id} (expected w-YYYY-MM-DD-NNN or w-YYYY-MM-DD-<author>-NNN)`,
166
+ );
134
167
  err.exitCode = 1;
135
168
  throw err;
136
169
  }
@@ -157,6 +190,12 @@ export async function setWorkStatus(id, status) {
157
190
  * List work items. Filters: status, category, project, owner, assignee,
158
191
  * mine, theirs, all, paName (used for --mine resolution).
159
192
  * By default excludes done + archived (mirrors desk-list semantics).
193
+ *
194
+ * Reads any well-formed work item (both id forms). Skips files whose
195
+ * filename does not parse as a work-item id at all — the same
196
+ * skip-unparseable posture the original had, restated against the
197
+ * shared parser so an in-directory README or a stray temp file cannot
198
+ * poison the listing.
160
199
  */
161
200
  export async function listWork(filters = {}) {
162
201
  const repoRoot = await getRepoRoot();
@@ -164,10 +203,11 @@ export async function listWork(filters = {}) {
164
203
  const files = await listJsonFiles(dir);
165
204
  const items = [];
166
205
  for (const f of files) {
206
+ if (!parseWorkItemId(f)) continue;
167
207
  try {
168
208
  items.push(await readJson(join(dir, f)));
169
209
  } catch {
170
- // Skip unparseable.
210
+ // Skip unparseable JSON contents.
171
211
  }
172
212
  }
173
213
  let filtered = items;
File without changes
File without changes
File without changes
File without changes
@@ -9,8 +9,8 @@
9
9
  "properties": {
10
10
  "id": {
11
11
  "type": "string",
12
- "pattern": "^w-\\d{4}-\\d{2}-\\d{2}-\\d{3}$",
13
- "description": "Work item identifier in the format w-YYYY-MM-DD-NNN"
12
+ "pattern": "^w-\\d{4}-\\d{2}-\\d{2}(?:-[a-z]{2,12})?-\\d{3}$",
13
+ "description": "Work item identifier. Two forms are permanently valid: legacy `w-YYYY-MM-DD-NNN` (pre-2026-08-12 items) and writer-scoped `w-YYYY-MM-DD-<author>-NNN` (post-R1 mints; author = /^[a-z]{2,12}$/ from operator/config/authors.json). New allocators must always emit the writer-scoped form — see packages/core/src/workItemId/index.mjs."
14
14
  },
15
15
  "projectId": {
16
16
  "type": ["string", "null"],
@@ -0,0 +1,219 @@
1
+ // workItemId/index.mjs — the canonical work-item-id primitive.
2
+ //
3
+ // One place, every rule. Every writer on the estate MUST parse work-item
4
+ // ids through `parseWorkItemId` (accepts both legal forms) and MUST mint
5
+ // new ids through `buildWorkItemId` (always the writer-scoped form).
6
+ // Nothing outside this module gets to know the shape of an id, and
7
+ // nothing outside this module gets to decide what the current writer's
8
+ // author token is.
9
+ //
10
+ // ────────────────────────────────────────────────────────────────────────
11
+ // Background — w-2026-08-12-001, R1 of the library remediation train.
12
+ //
13
+ // The estate has ≥5 autonomous writers competing for one filename space
14
+ // (`work/w-YYYY-MM-DD-NNN.json`): Dave's macbook session, Dex's
15
+ // wsd-laptop session, librarian-pr-intake on ops-01, upstream-triage on
16
+ // smarthome01, Baz manual. Each allocated the next-free NNN from its own
17
+ // locally-visible git state, and sync latency meant visible state lied.
18
+ // Four dual-mint collisions in two weeks; the 2026-08-11 collision
19
+ // wedged the ops-01 librarian sweep for 2+ hours (see incident
20
+ // `2026-08-11-wedge-root-cause-d024.md` and library architecture review
21
+ // `2026-08-11-library-architecture-review.md` §2.4 collision surface).
22
+ //
23
+ // Baz ratified the fix 2026-08-11: writer-scoped per-day sequences. Each
24
+ // writer gets its own NNN space, keyed by an author token from the
25
+ // authors registry (`operator/config/authors.json`). Cross-writer
26
+ // collision is now structurally impossible with zero coordination.
27
+ // Legacy ids without an author segment remain valid PARSE targets
28
+ // forever (forward-only migration; existing items never renumbered) but
29
+ // no allocator may ever MINT them again.
30
+ //
31
+ // ────────────────────────────────────────────────────────────────────────
32
+ // The two legal forms
33
+ //
34
+ // legacy: w-YYYY-MM-DD-NNN e.g. w-2026-08-11-025
35
+ // new: w-YYYY-MM-DD-<author>-NNN e.g. w-2026-08-12-dave-001
36
+ //
37
+ // Author token = /^[a-z]{2,12}$/ (lowercase letters only, 2–12 chars).
38
+ // Deliberately all-alpha: keeps the parse alternation unambiguous
39
+ // against the NNN segment (any middle segment starting with a digit is
40
+ // legacy) and mirrors the registry's `dave`/`dex`/`sam`/`max`/`ian`/
41
+ // `baz` shape without inviting alphanumeric drift.
42
+ //
43
+ // NNN = /^\d{3}$/, 001–999, zero-padded. NNN space is per-author-per-day,
44
+ // which is why we never run out of headroom in practice.
45
+
46
+ const AUTHOR_TOKEN_RE = /^[a-z]{2,12}$/;
47
+
48
+ // Two-branch alternation — the ONE-line spec of every valid id in the
49
+ // estate. Every other regex in this repo is either this one, or (for
50
+ // filenames) this one with a `\.json` suffix.
51
+ const WORK_ITEM_ID_RE = /^w-(\d{4}-\d{2}-\d{2})(?:-([a-z]{2,12}))?-(\d{3})$/;
52
+ const WORK_ITEM_FILENAME_RE = /^w-(\d{4}-\d{2}-\d{2})(?:-([a-z]{2,12}))?-(\d{3})\.json$/;
53
+
54
+ /**
55
+ * True iff `s` is a well-formed author token.
56
+ * @param {unknown} s
57
+ * @returns {boolean}
58
+ */
59
+ export function isValidAuthorToken(s) {
60
+ return typeof s === 'string' && AUTHOR_TOKEN_RE.test(s);
61
+ }
62
+
63
+ /**
64
+ * Parse a work-item id (with or without `.json`). Accepts both the
65
+ * legacy `w-YYYY-MM-DD-NNN` form and the writer-scoped
66
+ * `w-YYYY-MM-DD-<author>-NNN` form. Returns `null` if the string is
67
+ * not a valid id in either form — callers decide how to handle that.
68
+ *
69
+ * @param {string} idOrFilename
70
+ * @returns {{ date: string, author: string | null, nnn: number, id: string, isLegacy: boolean } | null}
71
+ */
72
+ export function parseWorkItemId(idOrFilename) {
73
+ if (typeof idOrFilename !== 'string') return null;
74
+ const bare = idOrFilename.endsWith('.json')
75
+ ? idOrFilename.slice(0, -'.json'.length)
76
+ : idOrFilename;
77
+ const m = bare.match(WORK_ITEM_ID_RE);
78
+ if (!m) return null;
79
+ const date = m[1];
80
+ const author = m[2] || null;
81
+ const nnn = parseInt(m[3], 10);
82
+ return { date, author, nnn, id: bare, isLegacy: author === null };
83
+ }
84
+
85
+ /**
86
+ * True iff `s` is a well-formed work-item id (either form). Convenience
87
+ * wrapper around `parseWorkItemId` for the many callers that only need
88
+ * a yes/no.
89
+ *
90
+ * @param {unknown} s
91
+ * @returns {boolean}
92
+ */
93
+ export function isValidWorkItemId(s) {
94
+ return parseWorkItemId(String(s)) !== null;
95
+ }
96
+
97
+ /**
98
+ * Mint a writer-scoped id. NEW writes MUST always go through this
99
+ * function; the legacy form is parse-only and no allocator may re-emit
100
+ * it. Throws if `author` is not a valid token — that is a caller bug
101
+ * (an allocator with no resolved author identity), not a runtime input.
102
+ *
103
+ * @param {object} opts
104
+ * @param {string} opts.date YYYY-MM-DD
105
+ * @param {string} opts.author registry-validated author token
106
+ * @param {number} opts.nnn 1..999
107
+ * @returns {string}
108
+ */
109
+ export function buildWorkItemId({ date, author, nnn }) {
110
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date))) {
111
+ throw new Error(`buildWorkItemId: bad date: ${date}`);
112
+ }
113
+ if (!isValidAuthorToken(author)) {
114
+ throw new Error(
115
+ `buildWorkItemId: bad author token: ${JSON.stringify(author)} (expected /^[a-z]{2,12}$/)`,
116
+ );
117
+ }
118
+ if (!Number.isInteger(nnn) || nnn < 1 || nnn > 999) {
119
+ throw new Error(`buildWorkItemId: nnn out of range: ${nnn} (expected 1..999)`);
120
+ }
121
+ return `w-${date}-${author}-${String(nnn).padStart(3, '0')}`;
122
+ }
123
+
124
+ /**
125
+ * Resolve the current caller's author token. Precedence, highest first:
126
+ *
127
+ * 1. `opts.explicit` — an explicit token passed by the caller
128
+ * (e.g. a HQ tool that KNOWS it is `max`
129
+ * or `ian`). ALWAYS wins.
130
+ * 2. `PA_AUTHOR_TOKEN` env — session/host default.
131
+ * 3. `opts.preferencesPaName` — `paName` from
132
+ * `operator/config/preferences.json`,
133
+ * the interactive-session fallback.
134
+ *
135
+ * Hard-errors with an actionable message naming every source if none
136
+ * resolves. Rationale: an allocator without a resolved author is a
137
+ * design bug — silently defaulting to a hard-coded token would put us
138
+ * back in the collision class this whole R1 exists to close.
139
+ *
140
+ * The precedence ordering is load-bearing: HQ tools deployed on ops-01
141
+ * (which shares $HOME across many launch contexts) MUST be able to pin
142
+ * their own token via the explicit param without an ambient
143
+ * `PA_AUTHOR_TOKEN` inherited from the parent shell silently
144
+ * overriding them. Tested by
145
+ * `test/workItemId/resolveAuthorToken.test.mjs`.
146
+ *
147
+ * @param {object} [opts]
148
+ * @param {string} [opts.explicit]
149
+ * @param {string} [opts.envToken] — inject env for tests; defaults to process.env.PA_AUTHOR_TOKEN
150
+ * @param {string} [opts.preferencesPaName] — `paName` field from preferences.json, if loaded
151
+ * @returns {string}
152
+ */
153
+ export function resolveAuthorToken(opts = {}) {
154
+ const envToken = 'envToken' in opts ? opts.envToken : process.env.PA_AUTHOR_TOKEN;
155
+ const candidates = [
156
+ { source: 'explicit param', value: opts.explicit },
157
+ { source: 'PA_AUTHOR_TOKEN env var', value: envToken },
158
+ { source: 'preferences.json paName', value: opts.preferencesPaName },
159
+ ];
160
+ for (const { source, value } of candidates) {
161
+ if (value == null || value === '') continue;
162
+ if (!isValidAuthorToken(value)) {
163
+ throw new Error(
164
+ `resolveAuthorToken: ${source} is not a valid author token: ${JSON.stringify(value)} (expected /^[a-z]{2,12}$/)`,
165
+ );
166
+ }
167
+ return value;
168
+ }
169
+ throw new Error(
170
+ 'resolveAuthorToken: no author token resolved. Set one of, in order of precedence: ' +
171
+ '(1) an explicit token via the caller\'s API, ' +
172
+ '(2) the PA_AUTHOR_TOKEN environment variable, ' +
173
+ '(3) `paName` in operator/config/preferences.json. ' +
174
+ 'Token must match /^[a-z]{2,12}$/. Registry lives at operator/config/authors.json.',
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Compute the highest NNN already allocated for a given `date` +
180
+ * `author` slot from a bag of parsed work-item filenames. Legacy ids
181
+ * (no author segment) DO NOT count toward any author's sequence — each
182
+ * writer's per-day NNN space starts from 001 on the day writer-scoping
183
+ * takes effect, and legacy items just occupy a separate, older space.
184
+ *
185
+ * Purpose: the arithmetic every allocator does is identical; keep the
186
+ * rule in one place so a new writer added to the registry doesn't need
187
+ * to re-implement it.
188
+ *
189
+ * @param {string[]} filenames raw directory entries
190
+ * @param {string} date YYYY-MM-DD
191
+ * @param {string} author the author whose sequence is being read
192
+ * @returns {number} 0 if the author has no items yet today
193
+ */
194
+ export function maxNForAuthorOnDate(filenames, date, author) {
195
+ if (!isValidAuthorToken(author)) {
196
+ throw new Error(
197
+ `maxNForAuthorOnDate: bad author: ${JSON.stringify(author)} (expected /^[a-z]{2,12}$/)`,
198
+ );
199
+ }
200
+ let max = 0;
201
+ for (const name of filenames) {
202
+ const parsed = parseWorkItemId(name);
203
+ if (!parsed) continue;
204
+ if (parsed.date !== date) continue;
205
+ if (parsed.author !== author) continue;
206
+ if (parsed.nnn > max) max = parsed.nnn;
207
+ }
208
+ return max;
209
+ }
210
+
211
+ /**
212
+ * The regex sources, exported for callers that MUST use a raw string
213
+ * (JSON Schema `pattern`, other language regexes). Do not reach into
214
+ * `.source` in JavaScript — call `parseWorkItemId`/`isValidWorkItemId`
215
+ * instead.
216
+ */
217
+ export const WORK_ITEM_ID_PATTERN = WORK_ITEM_ID_RE.source;
218
+ export const WORK_ITEM_FILENAME_PATTERN = WORK_ITEM_FILENAME_RE.source;
219
+ export const AUTHOR_TOKEN_PATTERN = AUTHOR_TOKEN_RE.source;
File without changes
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.20",
4
- "description": "Digital Operator PA — single-bundled public package surface (bins + library) for the platform.",
3
+ "version": "0.1.21",
4
+ "description": "Digital Operator PA \u2014 single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
7
7
  "exports": {
@@ -49,10 +49,11 @@
49
49
  "engines": {
50
50
  "node": ">=24"
51
51
  },
52
- "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper — see packages/cli/test/secrets. Counts must never lie.",
53
52
  "scripts": {
54
53
  "build": "node build.mjs",
54
+ "prepack": "node build.mjs",
55
55
  "test": "node --test --test-isolation=none test/*.test.mjs",
56
56
  "dry-publish": "node build.mjs && node scripts/dry-publish.mjs"
57
- }
58
- }
57
+ },
58
+ "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper \u2014 see packages/cli/test/secrets. Counts must never lie."
59
+ }