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 +6 -0
- package/commands/gcf.js +8 -4
- package/extension.json +1 -1
- package/lib/aif-config.js +27 -9
- package/lib/board.js +33 -15
- package/lib/config-set.js +119 -40
- package/lib/domain.js +370 -0
- package/lib/transitions.js +5 -130
- package/package.json +1 -1
- package/templates/agents-block.md +1 -1
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
|
|
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
|
|
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')).
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
10
|
-
* language is a preference, and a broken
|
|
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
|
|
21
|
+
export async function readAifConfig(projectDir) {
|
|
13
22
|
try {
|
|
14
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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'
|
|
269
|
-
|
|
270
|
-
|
|
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)
|
|
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
|
|
308
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
143
|
-
* its default. The file is read and checked
|
|
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
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
+
}
|
package/lib/transitions.js
CHANGED
|
@@ -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
|
@@ -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
|
|