gcf-board 0.2.19 → 0.3.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/README.md CHANGED
@@ -5,7 +5,7 @@ A local web board for game projects on [GCF](../gcf-aif-ext): the items by statu
5
5
  ## Requirements
6
6
 
7
7
  - Node.js ≥ 22.18
8
- - [`ai-factory`](https://www.npmjs.com/package/ai-factory) installed globally, and in each project [`gcf-aif-ext`](../gcf-aif-ext) ≥ 0.6 (`gcf board --json` with `schema: 1`)
8
+ - [`ai-factory`](https://www.npmjs.com/package/ai-factory) installed globally, and in each project [`gcf-aif-ext`](../gcf-aif-ext) ≥ 0.9 (`gcf board --json` with `schema: 2`: the columns and the item moves come from it as data)
9
9
  - for runs: [`gcf-harness-aif-ext`](../gcf-harness-aif-ext) ≥ 0.8 and Claude Code (`claude -p`)
10
10
  - gcf-board 0.2 needs `gcf-aif-ext` ≥ 0.7 (priority, questions, session marks, `run_mode`)
11
11
 
@@ -13,7 +13,7 @@ import { briefError, runCli } from './cli.js';
13
13
  import { broadcast, planRev, subscribe, watchProject } from './events.js';
14
14
  import { followLog, readShown, shownShape } from './files.js';
15
15
  import { HttpError } from './http-error.js';
16
- import { moveArgs, runIds } from './moves.js';
16
+ import { isCenterMove, moveArgs, runIds } from './moves.js';
17
17
  import { addProject, listProjects, projectOf, removeProject } from './registry.js';
18
18
  import { cancelResume, closeReport, forgetProject, resumeQueue, runsOf, startRun, unqueue, } from './runs.js';
19
19
  const send = (res, status, body) => {
@@ -102,12 +102,15 @@ export function createApp(o) {
102
102
  watchers.set(p.name, watchProject(p));
103
103
  };
104
104
  const cli = (p, args, timeout) => runCli(o.entry, p.path, args, timeout);
105
+ // The last domain each project's board read gave: an item move is built from its form.
106
+ const domains = new Map();
105
107
  /** `gcf board --json` of a project, refused when its schema is not the one this board reads. */
106
108
  const boardOf = async (p) => {
107
109
  const board = jsonOf(await cli(p, ['gcf', 'board', '--json']), 'gcf board');
108
110
  if (board.schema !== BOARD_SCHEMA)
109
111
  throw new HttpError(502, `gcf board gives schema ${board.schema ?? 'none'}, this board reads ${BOARD_SCHEMA}: ` +
110
112
  'update gcf-aif-ext and gcf-board together');
113
+ domains.set(p.name, board.domain);
111
114
  return board;
112
115
  };
113
116
  const routes = [
@@ -132,6 +135,7 @@ export function createApp(o) {
132
135
  async (_req, res, m) => {
133
136
  await removeProject(m[1]);
134
137
  forgetProject(m[1]);
138
+ domains.delete(m[1]);
135
139
  watchers.get(m[1])?.();
136
140
  watchers.delete(m[1]);
137
141
  send(res, 200, { ok: true });
@@ -210,7 +214,11 @@ export function createApp(o) {
210
214
  /^\/api\/p\/([^/]+)\/move$/,
211
215
  async (req, res, m) => {
212
216
  const p = await projectOf(m[1]);
213
- const r = await cli(p, moveArgs(await bodyOf(req)));
217
+ const body = await bodyOf(req);
218
+ const domain = isCenterMove(body.cmd)
219
+ ? undefined
220
+ : (domains.get(p.name) ?? (await boardOf(p)).domain);
221
+ const r = await cli(p, moveArgs(body, domain?.moves));
214
222
  broadcast({ type: 'board', project: p.name });
215
223
  send(res, 200, { ok: r.ok, out: r.out.trim(), err: briefError(r.err) });
216
224
  },
@@ -1,6 +1,3 @@
1
- // The moves a board may run, each one `ai-factory gcf` command (Methodology §4.1, §11). Anything
2
- // else is refused: the board is a view, the commands own every rule. An operator's text goes
3
- // as one `--flag=value` argument, so no word of it reads as a flag.
4
1
  import { HttpError } from './http-error.js';
5
2
  const SLUG = /^[a-z0-9][a-z0-9-]*$/;
6
3
  function id(b, key = 'id') {
@@ -23,23 +20,17 @@ function required(b, key) {
23
20
  throw new HttpError(400, `${key} is required`);
24
21
  return v;
25
22
  }
26
- function oneOf(b, key, allowed) {
27
- const v = b[key];
28
- if (typeof v !== 'string' || !allowed.includes(v))
29
- throw new HttpError(400, `${key} must be ${allowed.join(' | ')}`);
30
- return v;
31
- }
32
23
  const opt = (b, key, flag = key) => {
33
24
  const v = text(b, key);
34
25
  return v === undefined ? [] : [`--${flag}=${v}`];
35
26
  };
36
- const many = (b, key) => {
27
+ const many = (b, key, flag = key) => {
37
28
  const v = b[key];
38
29
  if (v === undefined)
39
30
  return [];
40
31
  if (!Array.isArray(v) || v.some((x) => typeof x !== 'string'))
41
32
  throw new HttpError(400, `${key} must be a list of text`);
42
- return v.filter(Boolean).map((x) => `--${key}=${x}`);
33
+ return v.filter(Boolean).map((x) => `--${flag}=${x}`);
43
34
  };
44
35
  // The core limits a tag to 40 characters too.
45
36
  const TAG = /^[a-z0-9][a-z0-9:-]{0,39}$/;
@@ -55,7 +46,7 @@ const list = (b, key, re, what, flag = key) => {
55
46
  const idList = (b, key) => list(b, key, SLUG, 'ids');
56
47
  const tagList = (b, key, flag = key) => list(b, key, TAG, 'tags', flag);
57
48
  /** `--remove` / `--no-remove` when the operator chose; the command's default otherwise. */
58
- const remove = (b) => b.remove === true ? ['--remove'] : b.remove === false ? ['--no-remove'] : [];
49
+ const remove = (b, key = 'remove', flag = key) => b[key] === true ? [`--${flag}`] : b[key] === false ? [`--no-${flag}`] : [];
59
50
  const NEW_ITEM_OPTIONS = [
60
51
  'gate',
61
52
  'title',
@@ -73,35 +64,8 @@ const NEW_ITEM_OPTIONS = [
73
64
  'effort',
74
65
  ];
75
66
  const MOVES = {
76
- submit: (b) => ['submit', id(b), ...opt(b, 'unverified')],
77
- close: (b) => ['close', id(b), ...many(b, 'constraint'), ...opt(b, 'unverified')],
78
- verdict: (b) => [
79
- 'verdict',
80
- id(b),
81
- oneOf(b, 'verdict', ['keep', 'tune', 'kill']),
82
- ...opt(b, 'note'),
83
- ...opt(b, 'tags'),
84
- ...many(b, 'constraint'),
85
- ...remove(b),
86
- ],
87
- decide: (b) => [
88
- 'decide',
89
- id(b),
90
- oneOf(b, 'choice', ['go', 'no', 'wontfix', 'known', 'close', 'continue', 'kill']),
91
- ...opt(b, 'reason'),
92
- ...remove(b),
93
- ],
94
- block: (b) => ['block', id(b), `--reason=${required(b, 'reason')}`],
95
- unblock: (b) => ['unblock', id(b), ...opt(b, 'note')],
96
67
  // A session's mark taken off: the item no longer shows running (Board-PRD §3.2, kind c).
97
68
  release: (b) => ['release', id(b)],
98
- drop: (b) => [
99
- 'drop',
100
- id(b),
101
- `--reason=${required(b, 'reason')}`,
102
- ...opt(b, 'tags'),
103
- ...remove(b),
104
- ],
105
69
  'item-new': (b) => [
106
70
  'item',
107
71
  'new',
@@ -156,12 +120,32 @@ const MOVES = {
156
120
  return ['permissions', ...args];
157
121
  },
158
122
  };
159
- /** The `ai-factory` arguments of a move, or a 400 naming what is wrong. */
160
- export function moveArgs(b) {
161
- const cmd = typeof b.cmd === 'string' && Object.hasOwn(MOVES, b.cmd) ? b.cmd : null;
162
- if (!cmd)
163
- throw new HttpError(400, `unknown move: ${String(b.cmd)}`);
164
- return ['gcf', ...MOVES[cmd](b)];
123
+ /** An item's move by its domain form: the words with the item's id, then each field's flag. */
124
+ function domainMoveArgs(spec, b) {
125
+ const words = spec.args.map((w) => (w === '{id}' ? id(b) : w));
126
+ const flags = spec.fields.flatMap((f) => {
127
+ const flag = f.flag ?? f.key;
128
+ if (f.kind === 'list')
129
+ return many(b, f.key, flag);
130
+ if (f.kind === 'remove')
131
+ return remove(b, f.key, flag);
132
+ return f.required ? [`--${flag}=${required(b, f.key)}`] : opt(b, f.key, flag);
133
+ });
134
+ return [...words, ...flags];
135
+ }
136
+ /** Whether a move is the center's own: its arguments need no domain. */
137
+ export const isCenterMove = (cmd) => typeof cmd === 'string' && Object.hasOwn(MOVES, cmd);
138
+ /**
139
+ * The `ai-factory` arguments of a move, or a 400 naming what is wrong: a center move, or one of
140
+ * `moves`, the domain's forms from the project's board read.
141
+ */
142
+ export function moveArgs(b, moves = {}) {
143
+ const cmd = typeof b.cmd === 'string' ? b.cmd : '';
144
+ if (isCenterMove(cmd))
145
+ return ['gcf', ...MOVES[cmd](b)];
146
+ if (Object.hasOwn(moves, cmd))
147
+ return ['gcf', ...domainMoveArgs(moves[cmd], b)];
148
+ throw new HttpError(400, `unknown move: ${String(b.cmd)}`);
165
149
  }
166
150
  /** The ids of a run: one slug each, at least one. */
167
151
  export function runIds(b) {
@@ -1,11 +1,3 @@
1
1
  // The read model of `ai-factory gcf board --json` (Methodology §11): the board refuses a schema
2
2
  // it does not know. Types only, plus the version this board reads.
3
- export const BOARD_SCHEMA = 1;
4
- export const STATUSES = [
5
- 'ready',
6
- 'blocked',
7
- 'awaiting-verdict',
8
- 'awaiting-decision',
9
- 'unplayed',
10
- 'parked',
11
- ];
3
+ export const BOARD_SCHEMA = 2;