gcf-aif-ext 0.9.0 → 0.10.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
@@ -50,6 +50,12 @@ Updating preserves `gcf/`, `cnlp.config.json` and the pre-commit hook. Run `ai-f
50
50
 
51
51
  On Windows, run `extension add` and `extension update` from PowerShell or cmd, not Git Bash: the installer unpacks with `tar`, and Git Bash's GNU tar reads `C:\…` as a remote host (`Cannot connect to C: resolve failed`). The Windows `tar.exe` handles it.
52
52
 
53
+ ### 0.10.0
54
+
55
+ - `gcf board --json` is `schema: 3`: `domain` describes the whole GCF domain for a board (Methodology §11, Board-PRD decision 16). It names the harness (`gcf-harness` when the project has gcf-harness-aif-ext, else null), the agent's move `run` (offered first in `items[].moves` to a ready item without a green verify, only with the harness), every move's form with its full `ai-factory` words (`gcf …`) — the new item, the phase, the inbox, the answers, the run order and `depends` included —, the item's fields for the card and the drawer, its blocks (`sections`) and the Help terms. `depends` is offered to every item; `waits_for` is empty while a run drives the item. gcf-board 0.4.0 reads it; update both together.
56
+ - `gcf config set language.ui --value=ru` (and `language.artifacts`) sets the agents' language in AI Factory's `.ai-factory/config.yaml`: that key only, its comments kept, one `gcf(config)` commit (an uncommitted file is committed whole; a file git ignores is written, not committed, with a warning). An empty value takes the key off. `gcf config` and the board's settings list them.
57
+ - AGENTS.md: agents leave the language to the human. Run `ai-factory gcf init` after the update.
58
+
53
59
  ### 0.9.0
54
60
 
55
61
  - `gcf board --json` is `schema: 2`. The new `domain` field gives the board its columns in order (label, tooltip), the status runs start from, and the form of every item move: label, tooltip, command words, fields, and the drop zone that means it (Methodology §11, Board-PRD decision 16). The `columns` map (status → ids) is gone: filter `items` by `status`. gcf-board reads it from 0.3.0 on; an older board refuses schema 2, and gcf-board 0.3.0 refuses an older core: update both together.
package/commands/gcf.js CHANGED
@@ -1130,14 +1130,14 @@ function registerBoard(gcf) {
1130
1130
  function registerSettings(gcf) {
1131
1131
  const config = gcf
1132
1132
  .command('config')
1133
- .description("The human's settings of gcf/config.yml: list them, or set one (commit)")
1133
+ .description("The human's settings and the agents' language: list them, or set one (commit)")
1134
1134
  // A misspelt subcommand would read as a stray word and list the settings, as if set.
1135
1135
  .allowExcessArguments(false)
1136
1136
  .option('--json', 'machine output')
1137
1137
  .action(async (o) => {
1138
- const { gcfRoot, readConfig, readRawConfig } = await lib('gcf-paths');
1138
+ const { gcfRoot, readConfig } = await lib('gcf-paths');
1139
1139
  await readConfig(gcfRoot()); // a config the rules refuse says so here
1140
- const list = (await lib('config-set')).settingsOf(await readRawConfig(gcfRoot()));
1140
+ const list = await (await lib('config-set')).projectSettings(process.cwd());
1141
1141
  out(o.json, list, (x) =>
1142
1142
  x.map((s) => `${s.key}: ${s.value ?? '-'} # ${s.label}`).join('\n'),
1143
1143
  );
@@ -1157,7 +1157,11 @@ function registerSettings(gcf) {
1157
1157
  commit: o.commit,
1158
1158
  });
1159
1159
  const now = `${r.key}: ${r.value ?? 'default'}`;
1160
- console.log(r.unchanged ? `${now} (unchanged)` : `${now}${shortHash(r.hash)}`);
1160
+ // An ignored file is written but not committed: a worktree's agent does not read it.
1161
+ const note = r.ignored
1162
+ ? ' (git ignores the file: not committed, and agents in a worktree will not see it)'
1163
+ : '';
1164
+ console.log(r.unchanged ? `${now} (unchanged)` : `${now}${shortHash(r.hash)}${note}`);
1161
1165
  });
1162
1166
  const many = (v, prev = []) => [...prev, v];
1163
1167
  gcf
package/extension.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
3
3
  "name": "gcf-aif-ext",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "description": "Game Context Framework for AI Factory",
6
6
  "skills": [
7
7
  "skills/gcf-init",
package/lib/aif-config.js CHANGED
@@ -1,22 +1,40 @@
1
1
  // The language AI Factory sets in `.ai-factory/config.yaml` for what an agent writes to the
2
2
  // human: `language.ui`. GCF adds no setting of its own (Methodology §5.2.3); agents read the
3
- // file themselves, the core reads it only for the prompts it writes.
3
+ // file themselves, the core reads it for the prompts it writes and sets it for the board
4
+ // (`gcf config set language.ui`).
4
5
  import { readFile } from 'node:fs/promises';
5
6
  import path from 'node:path';
6
7
  import YAML from 'yaml';
7
8
 
9
+ /** AI Factory's config file, project-relative. */
10
+ export const AIF_CONFIG = '.ai-factory/config.yaml';
11
+
12
+ // A language name or code, one word in any script: anything else would break the prompt line it
13
+ // goes in.
14
+ export const LANGUAGE_RE = /^[\p{L}\p{N}_-]{2,32}$/u;
15
+
8
16
  /**
9
- * The project's `language.ui`, `en` by default. A missing or unreadable file gives `en`: the
10
- * language is a preference, and a broken one must not stop the work.
17
+ * `.ai-factory/config.yaml` as the file has it: `{}` when it is missing, unreadable or not a
18
+ * map. The language is a preference, and a broken file must not stop the work.
19
+ * @returns {Promise<Record<string, any>>}
11
20
  */
12
- export async function uiLanguage(projectDir) {
21
+ export async function readAifConfig(projectDir) {
13
22
  try {
14
- const file = path.join(path.resolve(projectDir), '.ai-factory', 'config.yaml');
15
- const ui = YAML.parse(await readFile(file, 'utf8'))?.language?.ui;
23
+ const raw = YAML.parse(await readFile(path.join(path.resolve(projectDir), AIF_CONFIG), 'utf8'));
16
24
 
17
- // A language name or code, one word: anything else would break the prompt line it goes in.
18
- return typeof ui === 'string' && /^[\w-]{2,32}$/.test(ui.trim()) ? ui.trim() : 'en';
25
+ return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
19
26
  } catch {
20
- return 'en';
27
+ return {};
21
28
  }
22
29
  }
30
+
31
+ /** A language of the parsed file (`ui`, `artifacts`), or null when it holds no language. */
32
+ export function languageIn(aif, key) {
33
+ const v = aif?.language?.[key];
34
+
35
+ return typeof v === 'string' && LANGUAGE_RE.test(v.trim()) ? v.trim() : null;
36
+ }
37
+
38
+ /** The project's `language.ui`, `en` by default or when the value is no language. */
39
+ export const uiLanguage = async (projectDir) =>
40
+ languageIn(await readAifConfig(projectDir), 'ui') ?? 'en';
package/lib/board.js CHANGED
@@ -5,7 +5,7 @@ import { readFile } from 'node:fs/promises';
5
5
  import { existsSync } from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { buildLedger } from './ledger.js';
8
- import { readConfig, readRawConfig } from './gcf-paths.js';
8
+ import { readConfig } from './gcf-paths.js';
9
9
  import { readLive } from './live.js';
10
10
  import { listInbox } from './inbox.js';
11
11
  import { phaseStatus } from './phase.js';
@@ -14,16 +14,17 @@ import { resolveInside } from './safe-path.js';
14
14
  import { wipOf } from './audit-core.js';
15
15
  import { runHolder } from './main-lock.js';
16
16
  import { ITEM_TYPES } from './validate-item.js';
17
- import { domainOf, movesOf } from './transitions.js';
17
+ import { movesOf } from './transitions.js';
18
+ import { domainOf, hasHarness, RUNS_FROM } from './domain.js';
18
19
  import { openDepends } from './work-ops.js';
19
20
  import { listQuestions } from './questions.js';
20
21
  import { sessionActive } from './session-mark.js';
21
22
  import { agentOf } from './agent-permissions.js';
22
- import { settingsOf } from './config-set.js';
23
+ import { projectSettings } from './config-set.js';
23
24
 
24
25
  // The version of the JSON below (Methodology §11): a board refuses one it does not know. Bump it
25
26
  // when a field changes meaning or goes away; a new field does not bump it.
26
- export const BOARD_SCHEMA = 2;
27
+ export const BOARD_SCHEMA = 3;
27
28
 
28
29
  /**
29
30
  * Plan progress `{tasks, done}` from its checkboxes and the plan's project path, both null
@@ -230,8 +231,19 @@ function runningKind(projectDir, id) {
230
231
  return sessionActive(projectDir, id) ? 'session' : null;
231
232
  }
232
233
 
234
+ /**
235
+ * What a board may offer (§4.1): nothing while a run drives the item; with the harness, the
236
+ * agent's run to an item that waits for one and has no green verify yet.
237
+ */
238
+ function offered(c, { running, verified, harness }) {
239
+ if (running) return [];
240
+ const runs = harness && c.data.status === RUNS_FROM && !verified;
241
+
242
+ return [...(runs ? ['run'] : []), ...movesOf(c.data)];
243
+ }
244
+
233
245
  /** One item of the board. */
234
- async function itemOf(projectDir, c, verified, ledger) {
246
+ async function itemOf(projectDir, c, { verified, ledger, harness }) {
235
247
  const id = String(c.data.id);
236
248
  // A trailer counts only while the item waits on main for its integrate.
237
249
  const ready = c.data.status === 'ready' && verified.has(id);
@@ -263,11 +275,13 @@ async function itemOf(projectDir, c, verified, ledger) {
263
275
  resumable,
264
276
  resume,
265
277
  verified: ready,
266
- // The dependencies the run gate would refuse a ready item on now (Board-PRD §5.2).
278
+ // The dependencies the run gate would refuse a ready item on now (Board-PRD §5.2); none
279
+ // while it runs, past the gate.
267
280
  waits_for:
268
- c.data.status === 'ready' ? openDepends(ledger, c.data.depends ?? [], { waiting: true }) : [],
269
- // What a board may offer (§4.1): nothing while a run drives the item.
270
- moves: running ? [] : movesOf(c.data),
281
+ c.data.status === 'ready' && !running
282
+ ? openDepends(ledger, c.data.depends ?? [], { waiting: true })
283
+ : [],
284
+ moves: offered(c, { running, verified: ready, harness }),
271
285
  file: `gcf/work/${c.name}`,
272
286
  log: logOf(projectDir, id),
273
287
  // Filled by board() from the inbox.
@@ -280,8 +294,11 @@ export async function board(projectDir) {
280
294
  const root = path.join(path.resolve(projectDir), 'gcf');
281
295
  const ledger = await buildLedger(projectDir);
282
296
  const verified = verifiedOf(projectDir);
297
+ const extensions = await extensionsOf(projectDir);
298
+ const harness = hasHarness(extensions);
283
299
  const items = [];
284
- for (const c of ledger.items) items.push(await itemOf(projectDir, c, verified, ledger));
300
+ for (const c of ledger.items)
301
+ items.push(await itemOf(projectDir, c, { verified, ledger, harness }));
285
302
  inRunOrder(items);
286
303
  // When the wait began: the time of the commit `since` names (Board-PRD §13).
287
304
  const times = commitTimes(
@@ -304,19 +321,20 @@ export async function board(projectDir) {
304
321
  run_mode: config.run_mode,
305
322
  // What the harness hands its agent runs (Harness-PRD §7): deny rules and the context limits.
306
323
  agent: agentOf(config),
307
- // The human's settings of config.yml, each with its value and default (`gcf config set`).
308
- settings: settingsOf(await readRawConfig(root)),
324
+ // The human's settings of config.yml and the agents' language, each with its value and
325
+ // default (`gcf config set`).
326
+ settings: await projectSettings(projectDir),
309
327
  context: {
310
328
  handoff_pct: config.context_handoff_pct,
311
329
  window: config.context_window,
312
330
  compact_pct: config.auto_compact_pct,
313
331
  },
314
332
  phase: await phaseOf(projectDir),
315
- extensions: await extensionsOf(projectDir),
333
+ extensions,
316
334
  // A malformed block reads with fields missing (live check names it): null, not absent.
317
335
  release: releaseOf(live.release),
318
- // The columns and the moves as data (Board-PRD decision 16): the board hard-codes neither.
319
- domain: domainOf(),
336
+ // The domain as data (Board-PRD decision 16): its columns, blocks, fields, moves and terms.
337
+ domain: domainOf(extensions),
320
338
  items,
321
339
  wip: wipOf(ledger.items, config),
322
340
  inbox,
package/lib/config-set.js CHANGED
@@ -1,25 +1,36 @@
1
1
  // `gcf config set` (GCF-PRD §12.4): the human's settings of gcf/config.yml, one key at a time,
2
2
  // so a board edits them through the core and never writes the file itself. A change is checked
3
3
  // by the same rules a read applies before it is written; the rest of the file keeps its keys
4
- // and comments. `phases:` and `agent:` have their own commands and are not set here.
4
+ // and comments. `phases:` and `agent:` have their own commands and are not set here. The
5
+ // language agents write in is AI Factory's (`.ai-factory/config.yaml`), set here for the board
6
+ // too, since AI Factory has no command for it (Board-PRD decision 12).
5
7
  import { readFile } from 'node:fs/promises';
6
8
  import { existsSync } from 'node:fs';
9
+ import { spawnSync } from 'node:child_process';
7
10
  import path from 'node:path';
8
11
  import { isDeepStrictEqual } from 'node:util';
9
12
  import YAML from 'yaml';
10
13
  import { commitMove } from './unit-ops.js';
14
+ import { withMainLock } from './main-lock.js';
11
15
  import { atomicWrite } from './safe-path.js';
12
- import { configOf, DEFAULT_CONFIG, gcfRoot, RUN_MODES } from './gcf-paths.js';
16
+ import { configOf, DEFAULT_CONFIG, gcfRoot, readRawConfig, RUN_MODES } from './gcf-paths.js';
17
+ import { AIF_CONFIG, LANGUAGE_RE, languageIn, readAifConfig } from './aif-config.js';
13
18
 
14
- /** @typedef {'number' | 'share' | 'mode' | 'command'} Kind */
19
+ /** @typedef {'number' | 'share' | 'mode' | 'command' | 'language'} Kind */
15
20
 
16
21
  // Thrown inside the move to leave without a write or a commit.
17
22
  const NO_CHANGE = new Error('no change');
18
23
 
19
- /** gcf/config.yml as a YAML document with a map on top: an edit keeps its comments. */
20
- export function configDoc(text) {
24
+ // The languages AI Factory's own setup offers; any other name or code is taken too.
25
+ const LANGUAGES = ['en', 'ru', 'de', 'fr', 'es', 'zh', 'ja', 'ko', 'pt', 'it'];
26
+
27
+ /** A YAML file as a document with a map on top: an edit keeps its comments. */
28
+ export function configDoc(text, name = 'gcf/config.yml') {
21
29
  const doc = YAML.parseDocument(text);
22
- if (doc.errors.length) throw new Error(`gcf/config.yml: ${doc.errors[0].message}`);
30
+ if (doc.errors.length) throw new Error(`${name}: ${doc.errors[0].message}`);
31
+ // A file of a list or a word on top is not a settings file: replacing it would lose it.
32
+ if (doc.contents != null && !YAML.isMap(doc.contents) && doc.contents.toJSON() != null)
33
+ throw new Error(`${name} must map its keys`);
23
34
  if (!YAML.isMap(doc.contents)) doc.contents = /** @type {any} */ (doc.createNode({}));
24
35
 
25
36
  return doc;
@@ -33,8 +44,24 @@ export function ensureMap(doc, keys, error) {
33
44
  doc.setIn(keys, doc.createNode({}));
34
45
  }
35
46
 
36
- /** @type {{ key: string, kind: Kind, label: string }[]} */
47
+ /** @type {{ key: string, kind: Kind, label: string, file?: string, options?: string[] }[]} */
37
48
  export const SETTINGS = [
49
+ {
50
+ key: 'language.ui',
51
+ kind: 'language',
52
+ file: AIF_CONFIG,
53
+ options: LANGUAGES,
54
+ label:
55
+ 'the language agents write to you in: questions, block reasons, inbox entries, lessons, ' +
56
+ 'titles',
57
+ },
58
+ {
59
+ key: 'language.artifacts',
60
+ kind: 'language',
61
+ file: AIF_CONFIG,
62
+ options: LANGUAGES,
63
+ label: 'the language of plans and of the bodies of items that are not design units',
64
+ },
38
65
  {
39
66
  key: 'run_mode',
40
67
  kind: 'mode',
@@ -95,28 +122,54 @@ export const SETTINGS = [
95
122
 
96
123
  const BY_KEY = new Map(SETTINGS.map((s) => [s.key, s]));
97
124
 
125
+ /** The default a setting holds while its file does not set it; the artifacts follow the ui. */
126
+ function defaultOf(s, aif) {
127
+ const [head, sub] = s.key.split('.');
128
+ if (s.key === 'language.artifacts') return languageIn(aif, 'ui') ?? 'en';
129
+ if (s.kind === 'language') return 'en';
130
+ if (sub) return null;
131
+
132
+ return head === 'run_mode' ? 'worktree' : (DEFAULT_CONFIG[head] ?? null);
133
+ }
134
+
98
135
  /**
99
- * Each setting with what the file sets (null: not set, the default holds) and its default:
100
- * what a settings screen shows. `raw` is the parsed config.yml, before the defaults.
136
+ * Each setting with what its file sets (null: not set, the default holds) and its default:
137
+ * what a settings screen shows. `raw` is the parsed config.yml, before the defaults; `aif` the
138
+ * parsed `.ai-factory/config.yaml`.
101
139
  */
102
- export function settingsOf(raw) {
140
+ export function settingsOf(raw, aif = {}) {
103
141
  return SETTINGS.map((s) => {
104
142
  const [head, sub] = s.key.split('.');
105
- const parent = raw?.[head];
143
+ const parent = (s.file ? aif : raw)?.[head];
106
144
  const value = sub
107
145
  ? parent && typeof parent === 'object'
108
146
  ? (parent[sub] ?? null)
109
147
  : null
110
148
  : (parent ?? null);
111
- const def = sub ? null : head === 'run_mode' ? 'worktree' : (DEFAULT_CONFIG[head] ?? null);
149
+ // AI Factory's file is not checked by GCF's rules: a value that is no language shows unset.
150
+ const shown = s.file ? languageIn(aif, sub) : value;
112
151
 
113
- return { ...s, value, default: def };
152
+ return { ...s, value: shown, default: defaultOf(s, aif) };
114
153
  });
115
154
  }
116
155
 
156
+ /** Every setting as the project's two files have them. */
157
+ export const projectSettings = async (projectDir) =>
158
+ settingsOf(await readRawConfig(gcfRoot(projectDir)), await readAifConfig(projectDir));
159
+
160
+ /** A language name or code; empty takes the key off, and AI Factory's default holds. */
161
+ function languageOf(setting, t) {
162
+ if (t && !LANGUAGE_RE.test(t))
163
+ throw new Error(`${setting.key} must be a language name or code (en, ru…), got ${t}`);
164
+
165
+ return t || null;
166
+ }
167
+
117
168
  /** The value of `text` for a setting of `kind`; an empty number goes back to its default. */
118
169
  function valueOf(setting, text) {
119
170
  const t = String(text ?? '').trim();
171
+ if (setting.kind === 'language') return languageOf(setting, t);
172
+
120
173
  if (setting.kind === 'command') {
121
174
  if (!t) throw new Error(`${setting.key}: name the command, or none`);
122
175
 
@@ -138,45 +191,71 @@ function valueOf(setting, text) {
138
191
  return n;
139
192
  }
140
193
 
194
+ /** Whether git ignores `file`: a commit cannot carry it, and a worktree does not have it. */
195
+ const ignoredByGit = (projectDir, file) =>
196
+ spawnSync('git', ['check-ignore', '-q', '--', file], { cwd: projectDir }).status === 0;
197
+
198
+ const trackedByGit = (projectDir, file) =>
199
+ spawnSync('git', ['ls-files', '--error-unmatch', '--', file], { cwd: projectDir }).status === 0;
200
+
141
201
  /**
142
- * Set one key of gcf/config.yml and commit gcf(config); a number set to nothing goes back to
143
- * its default. The file is read and checked under the move's lock.
202
+ * Set one key of gcf/config.yml, or the language of `.ai-factory/config.yaml`, and commit
203
+ * gcf(config); a number set to nothing goes back to its default. The file is read and checked
204
+ * under the move's lock. An AI Factory file git ignores is written, not committed (`ignored`).
144
205
  */
145
206
  export async function setConfig(projectDir, key, text, { commit = true } = {}) {
146
207
  const setting = BY_KEY.get(key);
147
208
  if (!setting) throw new Error(`no setting ${key}: ${SETTINGS.map((s) => s.key).join(' | ')}`);
148
209
  const value = valueOf(setting, text);
149
- const file = path.join(gcfRoot(projectDir), 'config.yml');
210
+ const name = setting.file ?? 'gcf/config.yml';
211
+ const file = setting.file
212
+ ? path.join(path.resolve(projectDir), setting.file)
213
+ : path.join(gcfRoot(projectDir), 'config.yml');
214
+ const ignored = Boolean(setting.file) && ignoredByGit(projectDir, file);
215
+ // AI Factory's setup may leave its file uncommitted: the first change commits it whole.
216
+ const inputs = setting.file && !ignored && !trackedByGit(projectDir, file) ? [file] : [];
150
217
  const keys = key.split('.');
151
- let raw;
152
218
  // A change that changes nothing (the same value, a reset of a key not set) commits nothing.
153
219
  let unchanged = false;
154
- const hash = await commitMove(projectDir, `gcf(config): ${key}`, {
155
- files: [file],
156
- mutate: async () => {
157
- const text = existsSync(file) ? await readFile(file, 'utf8') : '';
158
- const doc = configDoc(text);
159
- if (keys.length > 1)
160
- ensureMap(doc, [keys[0]], `gcf/config.yml: ${keys[0]} must map its keys`);
161
- if (value === null) doc.deleteIn(keys);
162
- else doc.setIn(keys, value);
163
- raw = doc.toJS();
164
- configOf(raw);
165
- const out = doc.toString();
166
- // The values, not the text: YAML's output may differ in form (a final newline, `{}`).
167
- if (isDeepStrictEqual(YAML.parse(text) ?? {}, raw ?? {})) {
168
- unchanged = true;
169
- throw NO_CHANGE;
170
- }
171
-
172
- await atomicWrite(file, out);
173
- },
174
- commit,
175
- }).catch((e) => {
220
+ const move = () =>
221
+ commitMove(projectDir, `gcf(config): ${key}`, {
222
+ files: [file],
223
+ inputs,
224
+ mutate,
225
+ commit: commit && !ignored,
226
+ });
227
+ const mutate = async () => {
228
+ const text = existsSync(file) ? await readFile(file, 'utf8') : '';
229
+ const doc = configDoc(text, name);
230
+ // A reset of a key whose parent is not there changes nothing: no empty map is written.
231
+ if (value === null && keys.length > 1 && !YAML.isMap(doc.getIn([keys[0]], true))) {
232
+ unchanged = true;
233
+ throw NO_CHANGE;
234
+ }
235
+
236
+ if (keys.length > 1) ensureMap(doc, [keys[0]], `${name}: ${keys[0]} must map its keys`);
237
+ if (value === null) doc.deleteIn(keys);
238
+ else doc.setIn(keys, value);
239
+ const raw = doc.toJS();
240
+ if (!setting.file) configOf(raw);
241
+ const out = doc.toString();
242
+ // The values, not the text: YAML's output may differ in form (a final newline, `{}`).
243
+ if (isDeepStrictEqual(YAML.parse(text) ?? {}, raw ?? {})) {
244
+ unchanged = true;
245
+ throw NO_CHANGE;
246
+ }
247
+
248
+ await atomicWrite(file, out);
249
+ };
250
+
251
+ // A move that commits nothing takes no lock in commitMove: the ignored file still needs one.
252
+ const hash = await (ignored && commit ? withMainLock(projectDir, move) : move()).catch((e) => {
176
253
  if (e !== NO_CHANGE) throw e;
177
254
 
178
255
  return null;
179
256
  });
257
+ // The change is made: a broken gcf/config.yml must not read as its failure.
258
+ const settings = await projectSettings(projectDir).catch(() => []);
180
259
 
181
- return { key, value, unchanged, settings: settingsOf(raw), hash };
260
+ return { key, value, unchanged, ignored, settings, hash };
182
261
  }
package/lib/domain.js ADDED
@@ -0,0 +1,370 @@
1
+ // The GCF domain of the board read (Board-PRD decision 16): what a board shows and offers for a
2
+ // GCF project, as data. Its columns, its blocks (phase, WIP, playtest, inbox…), the fields of an
3
+ // item, every move's form with the `ai-factory` words it runs, the terms of its Help. A board
4
+ // draws these and builds each command from `args` and `fields`; it hard-codes none of them.
5
+ import { ITEM_MOVES, STORED } from './transitions.js';
6
+ import { DEFAULT_GATE, GATES, ITEM_TYPES, SEVERITIES, UNIT_ITEM_TYPES } from './validate-item.js';
7
+ import { SOURCES } from './inbox.js';
8
+
9
+ // The status a run starts from; the order of its column is the run order.
10
+ export const RUNS_FROM = 'ready';
11
+
12
+ // The board's words for each stored status: a column's label and tooltip.
13
+ const COLUMN_TEXT = {
14
+ ready: ['ready', 'Waits for a run; the order here is the run order'],
15
+ blocked: ['blocked', 'Stopped: waits for an answer, an asset or a fix of a failed check'],
16
+ 'awaiting-verdict': ['awaiting verdict', 'Built; waits for a playtest and a keep, tune or kill'],
17
+ 'awaiting-decision': ['awaiting decision', 'Waits for the operator to decide: go, no, close…'],
18
+ unplayed: ['unplayed', 'Closed without a playtest; waits for one'],
19
+ parked: ['parked', 'A bug kept as a known issue; unblock takes it back to ready'],
20
+ };
21
+
22
+ const GATE_HELP = {
23
+ verdict: 'Closed by a playtest: keep, tune or kill',
24
+ auto: 'Closed by the run itself when its checks are green; no playtest',
25
+ decision: 'Closed by the operator: go, no, close or continue',
26
+ };
27
+
28
+ // The form of each move for a board: the `ai-factory` words with `{id}` for its target and
29
+ // `{ids}` for a list of items, and the fields the operator fills. A field goes as
30
+ // `--<flag or key>=<value>`; a list repeats it; `ids` and `tags` go as one comma list;
31
+ // `remove` is `--remove` / `--no-remove`, or the command's own rule when left empty; `slug` is
32
+ // the target id the operator types (`{id}`); a field with `when` goes only while another field
33
+ // holds one of those values.
34
+ const REASON = { key: 'reason', label: 'Reason', kind: 'longtext', required: true };
35
+ const OPTIONAL_REASON = { ...REASON, required: false };
36
+ const REMOVE = { key: 'remove', label: 'Removal item for its code', kind: 'remove' };
37
+ const CONSTRAINT = { key: 'constraint', label: 'Constraint lines', kind: 'list' };
38
+ // close and submit need a green gcf verify of the code as it is now; the operator may say why
39
+ // they go without one, and the reason goes into the move's commit.
40
+ const UNVERIFIED = {
41
+ key: 'unverified',
42
+ label: 'No green gcf verify? Why it goes anyway',
43
+ kind: 'text',
44
+ };
45
+
46
+ const verdict = (v, help, fields, danger) => ({
47
+ label: v,
48
+ help,
49
+ args: ['gcf', 'verdict', '{id}', v],
50
+ fields,
51
+ ...(danger && { danger }),
52
+ });
53
+ const decide = (choice, help, fields = [], danger) => ({
54
+ label: choice,
55
+ help,
56
+ args: ['gcf', 'decide', '{id}', choice],
57
+ fields,
58
+ ...(danger && { danger }),
59
+ });
60
+ const text = (key, label, extra = {}) => ({ key, label, kind: 'text', ...extra });
61
+
62
+ // An item's moves (§4.1), offered by `items[].moves`.
63
+ const ITEM_SPECS = {
64
+ submit: {
65
+ label: 'submit',
66
+ help: 'Built: on to awaiting verdict or awaiting decision, by its gate',
67
+ args: ['gcf', 'submit', '{id}'],
68
+ fields: [UNVERIFIED],
69
+ rare: true,
70
+ },
71
+ close: {
72
+ label: 'close',
73
+ help: 'Done, gate auto: to history; a mechanic or a resource waits unplayed',
74
+ args: ['gcf', 'close', '{id}'],
75
+ fields: [CONSTRAINT, UNVERIFIED],
76
+ rare: true,
77
+ },
78
+ 'verdict:keep': verdict('keep', 'Played and kept: it goes to history', [
79
+ { key: 'note', label: 'Note', kind: 'text' },
80
+ CONSTRAINT,
81
+ ]),
82
+ 'verdict:tune': verdict('tune', 'Played; needs a change: back to ready with the note', [
83
+ { key: 'note', label: 'What to change', kind: 'longtext', required: true },
84
+ ]),
85
+ 'verdict:kill': verdict(
86
+ 'kill',
87
+ 'Played and rejected: it goes to the graveyard',
88
+ [{ key: 'note', label: 'Why', kind: 'longtext' }, REMOVE],
89
+ true,
90
+ ),
91
+ 'decide:go': decide('go', 'Approved: it goes to history as decided'),
92
+ 'decide:no': decide(
93
+ 'no',
94
+ 'Not doing it: it goes to the graveyard',
95
+ [OPTIONAL_REASON, REMOVE],
96
+ true,
97
+ ),
98
+ 'decide:wontfix': decide(
99
+ 'wontfix',
100
+ 'A bug left as it is: it goes to the graveyard',
101
+ [OPTIONAL_REASON, REMOVE],
102
+ true,
103
+ ),
104
+ 'decide:known': decide('known', 'A bug kept as a known issue: it goes to parked'),
105
+ 'decide:close': decide('close', 'The feature is done: it goes to history'),
106
+ 'decide:continue': decide('continue', 'The feature goes on: back to ready for more children'),
107
+ 'decide:kill': decide(
108
+ 'kill',
109
+ 'The feature is dropped: it goes to the graveyard with its live children',
110
+ [OPTIONAL_REASON],
111
+ true,
112
+ ),
113
+ block: {
114
+ label: 'block',
115
+ help: 'Stop it until the operator answers',
116
+ args: ['gcf', 'block', '{id}'],
117
+ fields: [REASON],
118
+ },
119
+ unblock: {
120
+ label: 'unblock',
121
+ help: 'The answer is given: back to ready (a feature with a dead child: to a decision)',
122
+ args: ['gcf', 'unblock', '{id}'],
123
+ fields: [{ key: 'note', label: 'Answer or "file: <path>"', kind: 'longtext' }],
124
+ drag: RUNS_FROM,
125
+ },
126
+ drop: {
127
+ label: 'drop',
128
+ help: 'Off the board into the graveyard, from any status',
129
+ args: ['gcf', 'drop', '{id}'],
130
+ fields: [REASON, { key: 'tags', label: 'Graveyard tags', kind: 'text' }, REMOVE],
131
+ danger: true,
132
+ rare: true,
133
+ drag: 'graveyard',
134
+ },
135
+ depends: {
136
+ label: 'depends',
137
+ help: 'The items it waits for: a run starts only after they are played',
138
+ args: ['gcf', 'depends', '{id}'],
139
+ fields: [
140
+ { key: 'add', label: 'Add: ids it waits for', kind: 'ids' },
141
+ { key: 'remove', label: 'Remove: ids it no longer waits for', kind: 'ids' },
142
+ ],
143
+ rare: true,
144
+ },
145
+ };
146
+
147
+ // The agent's move: a session of the gcf-run skill builds the item, driven by the harness.
148
+ const RUN = {
149
+ kind: 'agent',
150
+ label: 'run',
151
+ help: 'An agent session builds it: plan, code, the checks, then submit or close',
152
+ skill: 'gcf-run',
153
+ args: [],
154
+ fields: [],
155
+ };
156
+
157
+ // The new item form (`gcf item new`): work that is not a design unit; a design unit enters work
158
+ // through the gcf-card skill and take.
159
+ const NEW_ITEM = {
160
+ label: 'create',
161
+ help: 'A new work item, committed; the core refuses a type the phase does not take',
162
+ args: ['gcf', 'item', 'new', '{id}', '--commit'],
163
+ fields: [
164
+ { key: 'id', label: 'Id (lowercase-hyphen slug)', kind: 'slug', required: true },
165
+ {
166
+ key: 'type',
167
+ label: 'Type',
168
+ kind: 'choice',
169
+ required: true,
170
+ default: 'tech',
171
+ options: ITEM_TYPES.filter((t) => !UNIT_ITEM_TYPES.includes(t)).map((t) => ({
172
+ value: t,
173
+ label: t,
174
+ help: `gate ${DEFAULT_GATE[t]} by default`,
175
+ })),
176
+ },
177
+ {
178
+ key: 'gate',
179
+ label: 'Gate',
180
+ kind: 'choice',
181
+ options: [
182
+ { value: '', label: 'by type' },
183
+ ...GATES.map((g) => ({ value: g, label: g, help: GATE_HELP[g] })),
184
+ ],
185
+ },
186
+ {
187
+ key: 'severity',
188
+ label: 'Severity',
189
+ kind: 'choice',
190
+ required: true,
191
+ options: SEVERITIES.map((s) => ({ value: s, label: s })),
192
+ when: { key: 'type', is: ['bug'] },
193
+ },
194
+ text('release', 'Version', { required: true, when: { key: 'type', is: ['release'] } }),
195
+ text('channel', 'Channel', { required: true, when: { key: 'type', is: ['release'] } }),
196
+ text('title', 'Title: one line, what it gives'),
197
+ text('system', 'Subsystems, comma-separated'),
198
+ text('depends', 'Depends on ids, comma-separated'),
199
+ text('parent', 'Parent feature id'),
200
+ text('milestone', 'Milestone'),
201
+ text('blocked', 'Start blocked: why (e.g. "file needed")'),
202
+ text('model', 'Model of its run (e.g. opus)'),
203
+ text('effort', 'Effort of its run (e.g. high)'),
204
+ // The inbox entry it comes from: the form fills it, the operator does not.
205
+ text('from', 'From inbox entry', { hidden: true }),
206
+ ],
207
+ };
208
+
209
+ // The moves of the domain's blocks: the phase, the inbox, the questions, the run order.
210
+ const BLOCK_SPECS = {
211
+ 'item-new': NEW_ITEM,
212
+ 'phase-next': {
213
+ label: 'move on',
214
+ help: 'On to the next phase; against an unmet criterion only with a reason',
215
+ args: ['gcf', 'phase', 'next'],
216
+ fields: [
217
+ { key: 'reason', label: 'Reason (needed when a criterion is not met)', kind: 'longtext' },
218
+ ],
219
+ },
220
+ 'inbox-add': {
221
+ label: 'add',
222
+ help: 'A raw note for triage: an idea, a playtest note, a CI finding',
223
+ args: ['gcf', 'inbox', 'add', '--commit'],
224
+ fields: [
225
+ { key: 'text', label: 'Text', kind: 'text', required: true },
226
+ {
227
+ key: 'source',
228
+ label: 'Source',
229
+ kind: 'choice',
230
+ default: 'operator',
231
+ options: SOURCES.map((s) => ({ value: s, label: s })),
232
+ },
233
+ { key: 'tags', label: 'Tags', kind: 'tags' },
234
+ ],
235
+ },
236
+ 'inbox-drop': {
237
+ label: 'erase',
238
+ help: 'Noise: the entry is erased, nothing is made of it',
239
+ args: ['gcf', 'inbox', 'drop', '{id}', '--commit'],
240
+ fields: [],
241
+ danger: true,
242
+ },
243
+ 'inbox-tag': {
244
+ label: 'tag',
245
+ help: 'Sort the entry: tags added and removed',
246
+ args: ['gcf', 'inbox', 'tag', '{id}', '--commit'],
247
+ fields: [
248
+ { key: 'add', label: 'Add tags', kind: 'tags' },
249
+ { key: 'remove', label: 'Remove tags', kind: 'tags' },
250
+ ],
251
+ },
252
+ 'question-answer': {
253
+ label: 'answer',
254
+ help: 'The answer goes into the record; the next agent on its units puts it in place',
255
+ args: ['gcf', 'question', 'answer', '{id}'],
256
+ fields: [{ key: 'text', label: 'Answer', kind: 'text', required: true }],
257
+ },
258
+ prioritize: {
259
+ label: 'prioritize',
260
+ help: 'The run order: ranks 1, 2, 3… in one commit',
261
+ args: ['gcf', 'prioritize', '{ids}'],
262
+ fields: [],
263
+ },
264
+ };
265
+
266
+ // The item fields a board shows: on the card (`card`), in the drawer (all that have a value).
267
+ const GATE_FIELD_OPTIONS = GATES.map((g) => ({ value: g, label: g, help: GATE_HELP[g] }));
268
+ const FIELDS = [
269
+ { key: 'priority', label: 'priority', kind: 'rank', help: 'Rank: lower runs sooner', card: true },
270
+ { key: 'type', label: 'type', kind: 'text', card: true, badge: true },
271
+ {
272
+ key: 'gate',
273
+ label: 'gate',
274
+ kind: 'choice',
275
+ options: GATE_FIELD_OPTIONS,
276
+ help: 'Who closes it',
277
+ card: true,
278
+ badge: true,
279
+ },
280
+ { key: 'severity', label: 'severity', kind: 'text', card: true, badge: true },
281
+ { key: 'model', label: 'model', kind: 'text', card: true },
282
+ { key: 'effort', label: 'effort', kind: 'text' },
283
+ { key: 'tune_count', label: 'tune', kind: 'text', help: 'Tune verdicts so far', card: true },
284
+ {
285
+ key: 'waits_for',
286
+ label: 'waits for',
287
+ kind: 'ids',
288
+ help: 'The run gate refuses it until these are played',
289
+ card: true,
290
+ },
291
+ { key: 'depends', label: 'depends', kind: 'ids' },
292
+ { key: 'needed_by', label: 'needed by', kind: 'ids', help: 'The items that wait for this one' },
293
+ { key: 'system', label: 'system', kind: 'list' },
294
+ { key: 'parent', label: 'parent', kind: 'text' },
295
+ { key: 'children', label: 'children', kind: 'ids' },
296
+ { key: 'milestone', label: 'milestone', kind: 'text' },
297
+ { key: 'blocked', label: 'blocked', kind: 'longtext', help: 'Why it stopped', card: true },
298
+ ];
299
+
300
+ // The words of its Help (Board-PRD decision 17): what the board's terms mean in GCF.
301
+ const TERMS = [
302
+ ['item', 'One piece of work in gcf/work/: a file with its type, gate and status'],
303
+ ['gate', 'Who closes an item: a verdict (playtest), auto (its checks) or a decision'],
304
+ ...GATES.map((g) => [`gate: ${g}`, GATE_HELP[g]]),
305
+ [
306
+ 'feature',
307
+ 'A parent item: when its children are done, the operator decides: close, continue or kill',
308
+ ],
309
+ ['design unit', 'A mechanic, resource, rule, constraint or entity of gcf/design/'],
310
+ ['phase', 'The stage of the project (prototype, vertical slice…): the types it takes'],
311
+ ['WIP', 'Items built and waiting for a verdict; at the limit new verdict work stops'],
312
+ ['verify', 'The DoD: the project test and build commands, green on the code as it is'],
313
+ ['inbox', 'Raw input waiting for triage: it becomes an item or is erased'],
314
+ ['lesson', 'An inbox entry an agent session left: what got in its way'],
315
+ ['D1', 'A question only the operator answers; it blocks the units it names'],
316
+ ['D2', 'An assumption the agent made and works with; it comes back when its trigger fires'],
317
+ ['unplayed', 'Closed by its checks without a playtest; it waits for one'],
318
+ ['history', 'What was accepted: history.md'],
319
+ ['graveyard', 'What was killed or dropped, with the reason: graveyard.md'],
320
+ ].map(([term, help]) => ({ term, help }));
321
+
322
+ const SECTIONS = {
323
+ phase: { next: 'phase-next' },
324
+ release: {},
325
+ wip: { help: 'Items awaiting a verdict / wip_built_limit' },
326
+ inbox: { add: 'inbox-add', drop: 'inbox-drop', tag: 'inbox-tag' },
327
+ questions: { answer: 'question-answer' },
328
+ lessons: {},
329
+ playtest: {
330
+ waits: ['awaiting-verdict', 'unplayed'],
331
+ verdicts: ['verdict:keep', 'verdict:tune', 'verdict:kill'],
332
+ },
333
+ history: { label: 'Accepted' },
334
+ graveyard: { label: 'Graveyard' },
335
+ };
336
+
337
+ // The extension whose CLI drives agent runs (run, next, stop, metrics with --json).
338
+ const HARNESS = { extension: 'gcf-harness-aif-ext', cli: 'gcf-harness' };
339
+
340
+ /** Whether the project's AI Factory extensions include the harness (`extensions` of the read). */
341
+ export const hasHarness = (extensions) => Object.hasOwn(extensions ?? {}, HARNESS.extension);
342
+
343
+ /**
344
+ * The GCF domain of the board read. `extensions` are the project's AI Factory extensions: the
345
+ * agent's move, and the runner that drives it, only with the harness.
346
+ */
347
+ export function domainOf(extensions = {}) {
348
+ const harness = hasHarness(extensions);
349
+
350
+ return {
351
+ kind: 'gcf',
352
+ runs_from: RUNS_FROM,
353
+ harness: harness ? HARNESS.cli : null,
354
+ columns: STORED.map((status) => ({
355
+ status,
356
+ label: COLUMN_TEXT[status][0],
357
+ help: COLUMN_TEXT[status][1],
358
+ })),
359
+ moves: {
360
+ ...(harness && { run: RUN }),
361
+ ...Object.fromEntries(ITEM_MOVES.map((m) => [m, ITEM_SPECS[m]])),
362
+ ...BLOCK_SPECS,
363
+ },
364
+ fields: FIELDS,
365
+ terms: TERMS,
366
+ sections: SECTIONS,
367
+ new_item: 'item-new',
368
+ rank: 'prioritize',
369
+ };
370
+ }
@@ -28,8 +28,13 @@ const FROM = {
28
28
  block: ['ready'],
29
29
  unblock: ['blocked', 'parked'],
30
30
  drop: STORED,
31
+ // What it waits for may change at any status; the core refuses an unknown id and a cycle.
32
+ depends: STORED,
31
33
  };
32
34
 
35
+ /** Every move an item may be offered, in the board's order. */
36
+ export const ITEM_MOVES = Object.keys(FROM);
37
+
33
38
  /** True when `move` (`close`, `verdict:keep`, …) may start from `status`. */
34
39
  export function allowedFrom(move, status) {
35
40
  return (FROM[move] ?? []).includes(status);
@@ -67,133 +72,3 @@ export const movesOf = (data) =>
67
72
  export function isStorable(status) {
68
73
  return STORED.includes(status);
69
74
  }
70
-
71
- // The board's words for each stored status (Board-PRD decision 16): a column's label and tooltip.
72
- const COLUMN_TEXT = {
73
- ready: ['ready', 'Waits for a run; the order here is the run order'],
74
- blocked: ['blocked', 'Stopped: waits for an answer, an asset or a fix of a failed check'],
75
- 'awaiting-verdict': ['awaiting verdict', 'Built; waits for a playtest and a keep, tune or kill'],
76
- 'awaiting-decision': ['awaiting decision', 'Waits for the operator to decide: go, no, close…'],
77
- unplayed: ['unplayed', 'Closed without a playtest; waits for one'],
78
- parked: ['parked', 'A bug kept as a known issue; unblock takes it back to ready'],
79
- };
80
-
81
- // The form of each move (§4.1) for a board: the `ai-factory gcf` words with `{id}` for the item,
82
- // and the fields the operator fills. A field goes as `--<flag or key>=<value>`; a list repeats
83
- // it; `remove` is `--remove` / `--no-remove`, or the command's own rule when left empty.
84
- const REASON = { key: 'reason', label: 'Reason', kind: 'longtext', required: true };
85
- const OPTIONAL_REASON = { ...REASON, required: false };
86
- const REMOVE = { key: 'remove', label: 'Removal item for its code', kind: 'remove' };
87
- const CONSTRAINT = { key: 'constraint', label: 'Constraint lines', kind: 'list' };
88
- // close and submit need a green gcf verify of the code as it is now; the operator may say why
89
- // they go without one, and the reason goes into the move's commit.
90
- const UNVERIFIED = {
91
- key: 'unverified',
92
- label: 'No green gcf verify? Why it goes anyway',
93
- kind: 'text',
94
- };
95
-
96
- const verdict = (v, help, fields, danger) => ({
97
- label: v,
98
- help,
99
- args: ['verdict', '{id}', v],
100
- fields,
101
- ...(danger && { danger }),
102
- });
103
- const decide = (choice, help, fields = [], danger) => ({
104
- label: choice,
105
- help,
106
- args: ['decide', '{id}', choice],
107
- fields,
108
- ...(danger && { danger }),
109
- });
110
-
111
- const SPECS = {
112
- submit: {
113
- label: 'submit',
114
- help: 'Built: on to awaiting verdict or awaiting decision, by its gate',
115
- args: ['submit', '{id}'],
116
- fields: [UNVERIFIED],
117
- rare: true,
118
- },
119
- close: {
120
- label: 'close',
121
- help: 'Done, gate auto: to history; a mechanic or a resource waits unplayed',
122
- args: ['close', '{id}'],
123
- fields: [CONSTRAINT, UNVERIFIED],
124
- rare: true,
125
- },
126
- 'verdict:keep': verdict('keep', 'Played and kept: it goes to history', [
127
- { key: 'note', label: 'Note', kind: 'text' },
128
- CONSTRAINT,
129
- ]),
130
- 'verdict:tune': verdict('tune', 'Played; needs a change: back to ready with the note', [
131
- { key: 'note', label: 'What to change', kind: 'longtext', required: true },
132
- ]),
133
- 'verdict:kill': verdict(
134
- 'kill',
135
- 'Played and rejected: it goes to the graveyard',
136
- [{ key: 'note', label: 'Why', kind: 'longtext' }, REMOVE],
137
- true,
138
- ),
139
- 'decide:go': decide('go', 'Approved: it goes to history as decided'),
140
- 'decide:no': decide(
141
- 'no',
142
- 'Not doing it: it goes to the graveyard',
143
- [OPTIONAL_REASON, REMOVE],
144
- true,
145
- ),
146
- 'decide:wontfix': decide(
147
- 'wontfix',
148
- 'A bug left as it is: it goes to the graveyard',
149
- [OPTIONAL_REASON, REMOVE],
150
- true,
151
- ),
152
- 'decide:known': decide('known', 'A bug kept as a known issue: it goes to parked'),
153
- 'decide:close': decide('close', 'The feature is done: it goes to history'),
154
- 'decide:continue': decide('continue', 'The feature goes on: back to ready for more children'),
155
- 'decide:kill': decide(
156
- 'kill',
157
- 'The feature is dropped: it goes to the graveyard with its live children',
158
- [OPTIONAL_REASON],
159
- true,
160
- ),
161
- block: {
162
- label: 'block',
163
- help: 'Stop it until the operator answers',
164
- args: ['block', '{id}'],
165
- fields: [REASON],
166
- },
167
- unblock: {
168
- label: 'unblock',
169
- help: 'The answer is given: back to ready (a feature with a dead child: to a decision)',
170
- args: ['unblock', '{id}'],
171
- fields: [{ key: 'note', label: 'Answer or "file: <path>"', kind: 'longtext' }],
172
- drag: 'ready',
173
- },
174
- drop: {
175
- label: 'drop',
176
- help: 'Off the board into the graveyard, from any status',
177
- args: ['drop', '{id}'],
178
- fields: [REASON, { key: 'tags', label: 'Graveyard tags', kind: 'text' }, REMOVE],
179
- danger: true,
180
- rare: true,
181
- drag: 'graveyard',
182
- },
183
- };
184
-
185
- /**
186
- * The GCF domain of the board read (Board-PRD decision 16): its columns in order, the status a
187
- * run starts from, and the form of every move `movesOf` may name. A board draws these and
188
- * builds each command from `args` and `fields`; it hard-codes none of them.
189
- */
190
- export const domainOf = () => ({
191
- kind: 'gcf',
192
- runs_from: 'ready',
193
- columns: STORED.map((status) => ({
194
- status,
195
- label: COLUMN_TEXT[status][0],
196
- help: COLUMN_TEXT[status][1],
197
- })),
198
- moves: Object.fromEntries(Object.keys(FROM).map((m) => [m, SPECS[m]])),
199
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcf-aif-ext",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Game Context Framework for AI Factory",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@ gcf_forbidden:
34
34
  - do not answer a `TBD` of a design unit yourself: ask the human and offer your draft as the proposed answer
35
35
  - do not run `ai-factory gcf phase next`: the phase moves on the human's word only
36
36
  - do not change what agent runs may do: `ai-factory gcf permissions` and its deny groups belong to the human
37
- - do not change a setting of `gcf/config.yml`: `ai-factory gcf config set` belongs to the human
37
+ - do not change a setting of `gcf/config.yml` or the `language` of `.ai-factory/config.yaml`: `ai-factory gcf config set` belongs to the human
38
38
  - do not reuse an ended id: rework gets a new id with `reopens: <old id>`
39
39
  - do not commit past the gate with `--no-verify`: a refused commit names what to fix
40
40