@warpgogol/forge 2.21.1 → 2.21.5
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 +100 -0
- package/README.uk.md +100 -0
- package/os/mission/handlers/archive.test.ts +133 -3
- package/os/mission/handlers/archive.ts +86 -4
- package/os/mission/mission.module.ts +6 -0
- package/os/mission/types.ts +2 -1
- package/package.json +1 -1
- package/skills/meta/forge-bootstrap/SKILL.md +10 -2
- package/src/onboarding/create.ts +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,18 @@ Install https://npmjs.com/package/@warpgogol/forge in this folder and set up my
|
|
|
16
16
|
|
|
17
17
|
Replace `[describe your project]` with your idea — a game, a library, a knowledge base, anything. The AI agent installs Forge, scaffolds the project, and sets up a live preview. From there on, you just talk: describe what you want, and the agent builds it. No programming, no terminal, no commands.
|
|
18
18
|
|
|
19
|
+
### After the prompt completes — the Bootstrap skill
|
|
20
|
+
|
|
21
|
+
Once the AI agent has installed Forge and scaffolded your project, it will run the `/forge-bootstrap` skill. This is a required step that configures your project interactively:
|
|
22
|
+
|
|
23
|
+
- The language the AI uses to communicate with you
|
|
24
|
+
- The language for project documentation (RFCs, ADRs, READMEs) — defaults to English
|
|
25
|
+
- Your preferred working style — business or creative
|
|
26
|
+
- Your name and how you want to be addressed
|
|
27
|
+
- Your stack configuration (or migration of an existing project)
|
|
28
|
+
|
|
29
|
+
The skill asks a few simple questions in the chat, then sets everything up. After it completes, you're ready to create — just tell the AI agent what you want to build.
|
|
30
|
+
|
|
19
31
|
---
|
|
20
32
|
|
|
21
33
|
## What you can build with Forge
|
|
@@ -347,6 +359,94 @@ pnpm exec forge rfc.validate
|
|
|
347
359
|
pnpm exec forge skill.list
|
|
348
360
|
```
|
|
349
361
|
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Working with your AI agent
|
|
365
|
+
|
|
366
|
+
Forge works with any AI agent — Windsurf, Cursor, Claude Code, Codex CLI, or any IDE that supports agent skills. The setup is the same; only the conversation matters. Here are the patterns that make the biggest difference.
|
|
367
|
+
|
|
368
|
+
### Start with questions, not commands
|
|
369
|
+
|
|
370
|
+
The single most effective habit: before asking the agent to write code, ask it about the codebase. The agent can read files, search git history, and run Forge CLI commands — let it explore first.
|
|
371
|
+
|
|
372
|
+
Good questions to start a session:
|
|
373
|
+
|
|
374
|
+
- "How is this project structured?"
|
|
375
|
+
- "What skills are available and what do they do?"
|
|
376
|
+
- "Walk me through the RFC workflow in this project."
|
|
377
|
+
- "What did we change this week?" (the agent reads git log)
|
|
378
|
+
- "Why does this function have 15 parameters? Check git history."
|
|
379
|
+
|
|
380
|
+
This works for onboarding too — new team members can get up to speed by asking the agent questions instead of reading code manually. The agent reads everything locally; nothing is uploaded or trained on.
|
|
381
|
+
|
|
382
|
+
### AGENTS.md — your project's persistent memory
|
|
383
|
+
|
|
384
|
+
Forge generates `AGENTS.md` files at the project root and in each workspace directory. The agent reads them automatically at the start of every session — think of them as instructions that survive between conversations.
|
|
385
|
+
|
|
386
|
+
**What to put in AGENTS.md:**
|
|
387
|
+
|
|
388
|
+
- Build and test commands (`pnpm test`, `pnpm run build:check`)
|
|
389
|
+
- Code style conventions ("use 2 spaces, not tabs")
|
|
390
|
+
- Architecture decisions ("the game package owns all scene logic")
|
|
391
|
+
- "Do X, not Y" rules ("use `resolveImage()` for all image props, never raw `src`")
|
|
392
|
+
- Common mistakes to avoid
|
|
393
|
+
|
|
394
|
+
**When the agent makes a mistake**, tell it to update AGENTS.md so it doesn't repeat the error. Over time, AGENTS.md accumulates the team's hard-won knowledge.
|
|
395
|
+
|
|
396
|
+
Forge generates AGENTS.md from `forge.yaml` via `forge.agents.generate`. Hand-written AGENTS.md files (without the generated marker) are preserved. Generated ones are regenerated when you run `forge upgrade` or `forge.agents.generate` — so put your custom rules in `forge.yaml` or in a hand-written AGENTS.md, not in a generated one.
|
|
397
|
+
|
|
398
|
+
### Describe results, not steps
|
|
399
|
+
|
|
400
|
+
The agent is an autonomous worker, not a typewriter. Tell it what you want, not how to do it.
|
|
401
|
+
|
|
402
|
+
**Bad** (over-specified):
|
|
403
|
+
|
|
404
|
+
> Open src/game/player.ts, add a function called calculateScore that takes a number array, sum it, return the result. Then open src/game/player.test.ts and add a test.
|
|
405
|
+
|
|
406
|
+
**Good** (result-oriented):
|
|
407
|
+
|
|
408
|
+
> Add a scoring system that calculates total points from collected items.
|
|
409
|
+
|
|
410
|
+
For complex work, ask the agent to plan first:
|
|
411
|
+
|
|
412
|
+
> I want to add a multiplayer mode. Make a plan first, show it to me, and wait for my approval before implementing.
|
|
413
|
+
|
|
414
|
+
Forge's `/fo-idea` skill formalizes this: idea → audit → plan → implement → review. You can invoke it explicitly, or the agent will follow the pattern when the task is complex enough.
|
|
415
|
+
|
|
416
|
+
### Let the agent explore
|
|
417
|
+
|
|
418
|
+
Don't over-explain. The agent knows how to read files, search code, check git history, and run commands. Instead of pointing it to specific files, describe what you want to understand or change:
|
|
419
|
+
|
|
420
|
+
**Instead of:** "Look in the src/game folder for player.ts and check if it has a move function."
|
|
421
|
+
|
|
422
|
+
**Say:** "How does player movement work?"
|
|
423
|
+
|
|
424
|
+
The agent will find the relevant files, trace the call chain, and explain it back to you. If it needs more context, it will ask.
|
|
425
|
+
|
|
426
|
+
### Parallel work
|
|
427
|
+
|
|
428
|
+
You can run multiple agent sessions simultaneously — in separate IDE windows, terminal tabs, or git worktrees. Each session works independently.
|
|
429
|
+
|
|
430
|
+
This is useful for:
|
|
431
|
+
|
|
432
|
+
- Writing an RFC in one session while implementing a different feature in another
|
|
433
|
+
- Exploring two design approaches in parallel before committing to one
|
|
434
|
+
- Running a long validation in one session while continuing work in another
|
|
435
|
+
|
|
436
|
+
If you use the Werkstatt engine, its mission system keeps work isolated — each mission has its own workpiece directory, so parallel sessions don't conflict.
|
|
437
|
+
|
|
438
|
+
### Let the agent verify its own work
|
|
439
|
+
|
|
440
|
+
Give the agent criteria for "done" and let it check itself:
|
|
441
|
+
|
|
442
|
+
- "Run the tests after you're done."
|
|
443
|
+
- "Validate the RFC before committing."
|
|
444
|
+
- "Check project health with `forge doctor`."
|
|
445
|
+
|
|
446
|
+
Forge provides the tools — `forge doctor`, `forge rfc.validate`, `forge skill.list`, test runners. Tell the agent to use them. The agent can run commands, read the output, and fix issues without your intervention.
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
350
450
|
## Stack profiles
|
|
351
451
|
|
|
352
452
|
A stack profile defines the project scaffold: directory structure, dependencies, CI config, and first workspace. Choose a profile with `--profile` when creating a new project.
|
package/README.uk.md
CHANGED
|
@@ -16,6 +16,18 @@ Install https://npmjs.com/package/@warpgogol/forge in this folder and set up my
|
|
|
16
16
|
|
|
17
17
|
Замініть `[опишіть ваш проєкт]` на вашу ідею — гра, бібліотека, база знань, будь-що. ШІ-агент встановить Forge, згенерує каркас проєкту та налаштує живий перегляд. Далі ви просто розмовляєте: описуєте, що хочете, і агент будує це. Жодного програмування, жодного терміналу, жодних команд.
|
|
18
18
|
|
|
19
|
+
### Після виконання промпту — навичка Bootstrap
|
|
20
|
+
|
|
21
|
+
Коли ШІ-агент встановить Forge і згенерує каркас проєкту, він запустить навичку `/forge-bootstrap`. Це обов'язковий крок, який інтерактивно налаштовує ваш проєкт:
|
|
22
|
+
|
|
23
|
+
- Мову, якою ШІ спілкується з вами
|
|
24
|
+
- Мову документації проєкту (RFC, ADR, README) — за замовчуванням англійська
|
|
25
|
+
- Ваш стиль роботи — діловий або творчий
|
|
26
|
+
- Ваше ім'я та форму звертання
|
|
27
|
+
- Конфігурацію стеку (або міграцію наявного проєкту)
|
|
28
|
+
|
|
29
|
+
Навичка ставить кілька простих запитань у чаті, а потім налаштовує все. Після завершення ви готові творити — просто скажіть ШІ-агенту, що ви хочете створити.
|
|
30
|
+
|
|
19
31
|
---
|
|
20
32
|
|
|
21
33
|
## Що можна створити за допомогою Forge
|
|
@@ -338,6 +350,94 @@ pnpm exec forge rfc.validate
|
|
|
338
350
|
pnpm exec forge skill.list
|
|
339
351
|
```
|
|
340
352
|
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Робота з вашим ШІ-агентом
|
|
356
|
+
|
|
357
|
+
Forge працює з будь-яким ШІ-агентом — Windsurf, Cursor, Claude Code, Codex CLI або будь-яким IDE, що підтримує навички агентів. Налаштування однакове; важлива лише розмова. Ось патерни, які дають найбільший ефект.
|
|
358
|
+
|
|
359
|
+
### Починайте з питань, а не з команд
|
|
360
|
+
|
|
361
|
+
Найефективніша звичка: перед тим, як просити агента писати код, запитайте його про кодову базу. Агент може читати файли, шукати в історії git та запускати CLI-команди Forge — дайте йому спочатку дослідити.
|
|
362
|
+
|
|
363
|
+
Добрі питання для початку сесії:
|
|
364
|
+
|
|
365
|
+
- "Як структурований цей проєкт?"
|
|
366
|
+
- "Які навички доступні і що вони роблять?"
|
|
367
|
+
- "Проведи мене через RFC робочий процес у цьому проєкті."
|
|
368
|
+
- "Що ми змінили цього тижня?" (агент читає git log)
|
|
369
|
+
- "Чому ця функція має 15 параметрів? Перевір історію git."
|
|
370
|
+
|
|
371
|
+
Це працює і для онбордингу — нові члени команди можуть швидко ввійти в курс справи, запитуючи агента, замість того, щоб вручну читати код. Агент читає все локально; нічого не завантажується та не тренується на вашому коді.
|
|
372
|
+
|
|
373
|
+
### AGENTS.md — постійна пам'ять вашого проєкту
|
|
374
|
+
|
|
375
|
+
Forge генерує файли `AGENTS.md` в корені проєкту та в кожній директорії робочого простору. Агент читає їх автоматично на початку кожної сесії — сприймайте їх як інструкції, які зберігаються між розмовами.
|
|
376
|
+
|
|
377
|
+
**Що писати в AGENTS.md:**
|
|
378
|
+
|
|
379
|
+
- Команди збірки та тестування (`pnpm test`, `pnpm run build:check`)
|
|
380
|
+
- Угоди стилю коду ("використовуйте 2 пробіли, а не таби")
|
|
381
|
+
- Архітектурні рішення ("пакет гри володіє всією логікою сцен")
|
|
382
|
+
- Правила "роби X, а не Y" ("використовуйте `resolveImage()` для всіх image-пропів, ніколи сирий `src`")
|
|
383
|
+
- Типові помилки, яких слід уникати
|
|
384
|
+
|
|
385
|
+
**Коли агент помиляється**, скажіть йому оновити AGENTS.md, щоб він не повторював помилку. З часом AGENTS.md накопичує здобуті командою знання.
|
|
386
|
+
|
|
387
|
+
Forge генерує AGENTS.md з `forge.yaml` через `forge.agents.generate`. Написані вручну AGENTS.md (без згенерованого маркера) зберігаються. Згенеровані файли регенеруються при запуску `forge upgrade` або `forge.agents.generate` — тому кладіть свої правила в `forge.yaml` або в написаний вручну AGENTS.md, а не в згенерований.
|
|
388
|
+
|
|
389
|
+
### Описуйте результат, а не кроки
|
|
390
|
+
|
|
391
|
+
Агент — автономний працівник, а не друкарська машинка. Скажіть йому, що ви хочете, а не як це зробити.
|
|
392
|
+
|
|
393
|
+
**Погано** (занадто детально):
|
|
394
|
+
|
|
395
|
+
> Відкрий src/game/player.ts, додай функцію calculateScore, яка приймає масив чисел, сумує його, повертає результат. Потім відкрий src/game/player.test.ts і додай тест.
|
|
396
|
+
|
|
397
|
+
**Добре** (орієнтовано на результат):
|
|
398
|
+
|
|
399
|
+
> Додай систему підрахунку очок, яка обчислює загальні бали за зібрані предмети.
|
|
400
|
+
|
|
401
|
+
Для складних задач просіть агента спочатку спланувати:
|
|
402
|
+
|
|
403
|
+
> Я хочу додати багатокористувацький режим. Спочатку склади план, покажи мені, і чекай на моє схвалення перед реалізацією.
|
|
404
|
+
|
|
405
|
+
Навичка Forge `/fo-idea` формалізує це: ідея → аудит → план → реалізація → рев'ю. Ви можете викликати її явно, або агент буде слідувати цьому патерну, коли задача достатньо складна.
|
|
406
|
+
|
|
407
|
+
### Дайте агенту дослідити
|
|
408
|
+
|
|
409
|
+
Не пояснюйте занадто детально. Агент вміє читати файли, шукати в коді, перевіряти історію git та запускати команди. Замість того, щоб вказувати на конкретні файли, опишіть, що ви хочете зрозуміти або змінити:
|
|
410
|
+
|
|
411
|
+
**Замість:** "Подивись у папці src/game файл player.ts і перевір, чи є там функція move."
|
|
412
|
+
|
|
413
|
+
**Скажіть:** "Як працює рух гравця?"
|
|
414
|
+
|
|
415
|
+
Агент знайде відповідні файли, простежить ланцюг викликів і пояснить вам. Якщо йому потрібен буде додатковий контекст, він запитає.
|
|
416
|
+
|
|
417
|
+
### Паралельна робота
|
|
418
|
+
|
|
419
|
+
Ви можете запускати кілька сесій агента одночасно — в різних вікнах IDE, вкладках терміналу або git worktrees. Кожна сесія працює незалежно.
|
|
420
|
+
|
|
421
|
+
Це корисно для:
|
|
422
|
+
|
|
423
|
+
- Написання RFC в одній сесії, паралельно з реалізацією іншої функції в іншій
|
|
424
|
+
- Дослідження двох дизайн-підходів паралельно перед вибором одного
|
|
425
|
+
- Запуску тривалої валідації в одній сесії, продовжуючи роботу в іншій
|
|
426
|
+
|
|
427
|
+
Якщо ви використовуєте рушій Werkstatt, його система місій ізолює роботу — кожна місія має власну директорію workpiece, тому паралельні сесії не конфліктують.
|
|
428
|
+
|
|
429
|
+
### Дозвольте агенту перевіряти власну роботу
|
|
430
|
+
|
|
431
|
+
Дайте агенту критерії "готово" і дозвольте йому перевірити себе:
|
|
432
|
+
|
|
433
|
+
- "Запусти тести після завершення."
|
|
434
|
+
- "Валідуй RFC перед комітом."
|
|
435
|
+
- "Перевір стан проєкту через `forge doctor`."
|
|
436
|
+
|
|
437
|
+
Forge надає інструменти — `forge doctor`, `forge rfc.validate`, `forge skill.list`, тест-ранери. Скажіть агенту використовувати їх. Агент може запускати команди, читати вивід та виправляти проблеми без вашої втручання.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
341
441
|
## Профілі стеку
|
|
342
442
|
|
|
343
443
|
Профіль стеку визначає каркас проєкту: структуру директорій, залежності, конфігурацію CI та перший робочий простір. Оберіть профіль прапорцем `--profile` під час створення проєкту.
|
|
@@ -147,16 +147,16 @@ describe("mission.archive", () => {
|
|
|
147
147
|
expect(data.skipped.some((s) => s.reason === "destination exists")).toBe(true);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
test("unreadable manifest → skipped with '
|
|
150
|
+
test("unreadable manifest with empty dir → skipped with 'empty remnant' reason", async () => {
|
|
151
151
|
const missionDir = path.join(missionsDir, "test-m008");
|
|
152
152
|
await fs.mkdir(missionDir, { recursive: true });
|
|
153
|
-
// No mission.yaml —
|
|
153
|
+
// No mission.yaml, no workpiece — empty remnant
|
|
154
154
|
|
|
155
155
|
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
156
156
|
|
|
157
157
|
expect(data.moved).toHaveLength(0);
|
|
158
158
|
expect(data.skipped).toHaveLength(1);
|
|
159
|
-
expect(data.skipped[0].reason).toBe("
|
|
159
|
+
expect(data.skipped[0].reason).toBe("empty remnant — use --clean-orphans to remove");
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
test("open mission in archive/ → moved back to missions/ (bidirectional)", async () => {
|
|
@@ -388,4 +388,134 @@ describe("mission.archive", () => {
|
|
|
388
388
|
);
|
|
389
389
|
expect(installCall).toBeUndefined();
|
|
390
390
|
});
|
|
391
|
+
|
|
392
|
+
// RFC-0982: Fallback state detection and --clean-orphans tests
|
|
393
|
+
|
|
394
|
+
test("RFC-0982: orphaned workpiece with only .astro/ cache → state detected as closed, archived", async () => {
|
|
395
|
+
const missionDir = path.join(missionsDir, "test-r982-01");
|
|
396
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
397
|
+
await fs.mkdir(path.join(workpieceDir, ".astro"), { recursive: true });
|
|
398
|
+
await fs.writeFile(path.join(workpieceDir, ".astro", "cache.txt"), "cache\n");
|
|
399
|
+
// No mission.yaml — orphaned remnant
|
|
400
|
+
|
|
401
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
402
|
+
|
|
403
|
+
expect(data.moved).toHaveLength(1);
|
|
404
|
+
expect(data.moved[0].missionId).toBe("test-r982-01");
|
|
405
|
+
expect(data.moved[0].state).toBe("closed");
|
|
406
|
+
expect(data.moved[0].direction).toBe("into-archive");
|
|
407
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-r982-01"))).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("RFC-0982: .closed marker fallback → state detected as closed", async () => {
|
|
411
|
+
const missionDir = path.join(missionsDir, "test-r982-02");
|
|
412
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
413
|
+
await fs.mkdir(workpieceDir, { recursive: true });
|
|
414
|
+
await fs.writeFile(path.join(workpieceDir, ".closed"), "2026-08-29T12:00:00Z\n");
|
|
415
|
+
// Also add a source file so it's not cache-only
|
|
416
|
+
await fs.writeFile(path.join(workpieceDir, "src.ts"), "// source\n");
|
|
417
|
+
// No mission.yaml — but .closed marker present
|
|
418
|
+
|
|
419
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
420
|
+
|
|
421
|
+
expect(data.moved).toHaveLength(1);
|
|
422
|
+
expect(data.moved[0].missionId).toBe("test-r982-02");
|
|
423
|
+
expect(data.moved[0].state).toBe("closed");
|
|
424
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-r982-02"))).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("RFC-0982: --clean-orphans trashes orphaned remnant dir", async () => {
|
|
428
|
+
const missionDir = path.join(missionsDir, "test-r982-03");
|
|
429
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
430
|
+
await fs.mkdir(path.join(workpieceDir, ".astro"), { recursive: true });
|
|
431
|
+
await fs.writeFile(path.join(workpieceDir, ".astro", "cache.txt"), "cache\n");
|
|
432
|
+
// No mission.yaml — orphaned remnant with only cache
|
|
433
|
+
|
|
434
|
+
const data = unwrap(
|
|
435
|
+
await runMissionArchive(makeInput({ "clean-orphans": true }), makeContext(tmpDir)),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(data.moved).toHaveLength(1);
|
|
439
|
+
expect(data.moved[0].missionId).toBe("test-r982-03");
|
|
440
|
+
expect(data.moved[0].direction).toBe("trashed-orphan");
|
|
441
|
+
expect(data.moved[0].to).toBe("(trashed)");
|
|
442
|
+
expect(existsSync(missionDir)).toBe(false);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("RFC-0982: --clean-orphans --dry-run → reported but not trashed", async () => {
|
|
446
|
+
const missionDir = path.join(missionsDir, "test-r982-04");
|
|
447
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
448
|
+
await fs.mkdir(path.join(workpieceDir, ".astro"), { recursive: true });
|
|
449
|
+
await fs.writeFile(path.join(workpieceDir, ".astro", "cache.txt"), "cache\n");
|
|
450
|
+
|
|
451
|
+
const data = unwrap(
|
|
452
|
+
await runMissionArchive(
|
|
453
|
+
makeInput({ "clean-orphans": true, "dry-run": true }),
|
|
454
|
+
makeContext(tmpDir),
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(data.moved).toHaveLength(1);
|
|
459
|
+
expect(data.moved[0].direction).toBe("trashed-orphan");
|
|
460
|
+
expect(data.dryRun).toBe(true);
|
|
461
|
+
// Dir should still exist in dry-run
|
|
462
|
+
expect(existsSync(missionDir)).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("RFC-0982: --clean-orphans skips non-orphaned dir with mission.yaml", async () => {
|
|
466
|
+
await writeMissionManifest(missionsDir, "test-r982-05", "closed");
|
|
467
|
+
const workpieceDir = path.join(missionsDir, "test-r982-05", "workpiece");
|
|
468
|
+
await fs.mkdir(path.join(workpieceDir, ".astro"), { recursive: true });
|
|
469
|
+
|
|
470
|
+
const data = unwrap(
|
|
471
|
+
await runMissionArchive(makeInput({ "clean-orphans": true }), makeContext(tmpDir)),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
// Should be archived normally, not trashed
|
|
475
|
+
expect(data.moved).toHaveLength(1);
|
|
476
|
+
expect(data.moved[0].direction).toBe("into-archive");
|
|
477
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-r982-05"))).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("RFC-0982: skip reason 'manually inspect' for dir with non-cache content but no mission.yaml", async () => {
|
|
481
|
+
const missionDir = path.join(missionsDir, "test-r982-06");
|
|
482
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
483
|
+
await fs.mkdir(workpieceDir, { recursive: true });
|
|
484
|
+
// Source file in workpiece — non-cache content, no .closed marker
|
|
485
|
+
await fs.writeFile(path.join(workpieceDir, "src.ts"), "// source code\n");
|
|
486
|
+
// No mission.yaml
|
|
487
|
+
|
|
488
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
489
|
+
|
|
490
|
+
expect(data.moved).toHaveLength(0);
|
|
491
|
+
expect(data.skipped).toHaveLength(1);
|
|
492
|
+
expect(data.skipped[0].reason).toContain("manually inspect");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("RFC-0982: --clean-orphans trashes empty remnant dir (no workpiece, no mission.yaml)", async () => {
|
|
496
|
+
const missionDir = path.join(missionsDir, "test-r982-08");
|
|
497
|
+
await fs.mkdir(missionDir, { recursive: true });
|
|
498
|
+
// No mission.yaml, no workpiece — empty remnant, state would be null
|
|
499
|
+
|
|
500
|
+
const data = unwrap(
|
|
501
|
+
await runMissionArchive(makeInput({ "clean-orphans": true }), makeContext(tmpDir)),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(data.moved).toHaveLength(1);
|
|
505
|
+
expect(data.moved[0].missionId).toBe("test-r982-08");
|
|
506
|
+
expect(data.moved[0].direction).toBe("trashed-orphan");
|
|
507
|
+
expect(existsSync(missionDir)).toBe(false);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test("RFC-0982: skip reason 'use --clean-orphans' for empty remnant dir", async () => {
|
|
511
|
+
const missionDir = path.join(missionsDir, "test-r982-07");
|
|
512
|
+
await fs.mkdir(missionDir, { recursive: true });
|
|
513
|
+
// No mission.yaml, no workpiece, no content — empty remnant
|
|
514
|
+
|
|
515
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
516
|
+
|
|
517
|
+
expect(data.moved).toHaveLength(0);
|
|
518
|
+
expect(data.skipped).toHaveLength(1);
|
|
519
|
+
expect(data.skipped[0].reason).toContain("use --clean-orphans");
|
|
520
|
+
});
|
|
391
521
|
});
|
|
@@ -18,6 +18,7 @@ archive subdirectories back to missions/.
|
|
|
18
18
|
<item>RFC-0801: add service-folder cleanup (node_modules, dist, .astro, .wrangler, .cache, .turbo) before archive move.</item>
|
|
19
19
|
<item>RFC-0733: add pinned-files pre-check — skip pinned mission directories with warning instead of moving them.</item>
|
|
20
20
|
<item>RFC-0804: auto-refresh pnpm-lock.yaml after directory moves.</item>
|
|
21
|
+
<item>RFC-0982: fallback state detection for orphaned workpiece dirs (no mission.yaml) via .closed marker and cache-only heuristic; --clean-orphans flag; improved skip reasons.</item>
|
|
21
22
|
</CHANGE_SUMMARY>
|
|
22
23
|
*/
|
|
23
24
|
|
|
@@ -63,6 +64,11 @@ async function cleanServiceFolders(workpieceDir: string): Promise<string[]> {
|
|
|
63
64
|
return removed;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// RFC-0982: Shared cache-entry predicate — prevents filter logic divergence.
|
|
68
|
+
function isCacheEntry(name: string): boolean {
|
|
69
|
+
return name.startsWith(".") || name === "node_modules" || name === "dist";
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
async function readMissionState(missionDir: string): Promise<string | null> {
|
|
67
73
|
const manifestPath = path.join(missionDir, "mission.yaml");
|
|
68
74
|
try {
|
|
@@ -70,10 +76,64 @@ async function readMissionState(missionDir: string): Promise<string | null> {
|
|
|
70
76
|
const parsed = parseYaml(raw) as Record<string, unknown>;
|
|
71
77
|
const state = parsed?.state;
|
|
72
78
|
if (typeof state === "string") return state.trim();
|
|
73
|
-
return null;
|
|
74
79
|
} catch {
|
|
75
|
-
|
|
80
|
+
// Fall through to secondary checks
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// RFC-0982: Secondary — check for workpiece/.closed marker
|
|
84
|
+
const closedMarker = path.join(missionDir, "workpiece", ".closed");
|
|
85
|
+
if (existsSync(closedMarker)) return "closed";
|
|
86
|
+
|
|
87
|
+
// RFC-0982: Tertiary — if workpiece/ exists and contains only cache entries,
|
|
88
|
+
// treat as closed (post-close remnant pattern)
|
|
89
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
90
|
+
if (existsSync(workpieceDir)) {
|
|
91
|
+
const entries = await fs.readdir(workpieceDir);
|
|
92
|
+
const nonCacheEntries = entries.filter((e) => !isCacheEntry(e));
|
|
93
|
+
if (nonCacheEntries.length === 0) {
|
|
94
|
+
return "closed";
|
|
95
|
+
}
|
|
76
96
|
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// RFC-0982: Check if a mission directory is an orphaned remnant — no mission.yaml
|
|
102
|
+
// and workpiece/ contains only cache entries (or no workpiece at all).
|
|
103
|
+
async function isOrphanedRemnant(missionDir: string): Promise<boolean> {
|
|
104
|
+
const manifestPath = path.join(missionDir, "mission.yaml");
|
|
105
|
+
if (existsSync(manifestPath)) return false;
|
|
106
|
+
|
|
107
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
108
|
+
if (!existsSync(workpieceDir)) return true;
|
|
109
|
+
|
|
110
|
+
const entries = await fs.readdir(workpieceDir);
|
|
111
|
+
const nonCacheEntries = entries.filter((e) => !isCacheEntry(e));
|
|
112
|
+
return nonCacheEntries.length === 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// RFC-0982: Check if a mission directory has non-cache content (source files,
|
|
116
|
+
// configs, etc.) that warrants manual inspection rather than auto-cleanup.
|
|
117
|
+
async function hasNonCacheContent(missionDir: string): Promise<boolean> {
|
|
118
|
+
const entries = await fs.readdir(missionDir, { withFileTypes: true });
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry.name === "workpiece") continue;
|
|
121
|
+
if (entry.isDirectory() && isCacheEntry(entry.name)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Any file or non-cache directory at mission root is content
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check workpiece/ for non-cache content
|
|
129
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
130
|
+
if (existsSync(workpieceDir)) {
|
|
131
|
+
const wpEntries = await fs.readdir(workpieceDir);
|
|
132
|
+
const nonCacheEntries = wpEntries.filter((e) => !isCacheEntry(e));
|
|
133
|
+
if (nonCacheEntries.length > 0) return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
77
137
|
}
|
|
78
138
|
|
|
79
139
|
interface MoveAttempt {
|
|
@@ -158,6 +218,7 @@ export async function runMissionArchive(
|
|
|
158
218
|
|
|
159
219
|
const dryRun = context.dryRun || input.flags["dry-run"] === true;
|
|
160
220
|
const statusFilter = input.flags["status"] as string | undefined;
|
|
221
|
+
const cleanOrphans = input.flags["clean-orphans"] === true;
|
|
161
222
|
|
|
162
223
|
if (statusFilter && !MISSION_TERMINAL_STATUSES.includes(statusFilter as never)) {
|
|
163
224
|
throw new Error(
|
|
@@ -223,13 +284,35 @@ export async function runMissionArchive(
|
|
|
223
284
|
|
|
224
285
|
for (const missionId of rootDirs) {
|
|
225
286
|
const missionDir = path.join(missionsPath, missionId);
|
|
287
|
+
const sourceRel = `${MISSIONS_DIR}/${missionId}`;
|
|
288
|
+
|
|
289
|
+
// RFC-0982: --clean-orphans — trash orphaned remnant directories before state
|
|
290
|
+
// detection. This catches dirs where mission.yaml is missing and only cache
|
|
291
|
+
// entries remain, regardless of whether readMissionState returns "closed" or null.
|
|
292
|
+
if (cleanOrphans && (await isOrphanedRemnant(missionDir))) {
|
|
293
|
+
if (!dryRun) {
|
|
294
|
+
await trashPath(missionDir);
|
|
295
|
+
}
|
|
296
|
+
moved.push({
|
|
297
|
+
missionId,
|
|
298
|
+
state: "closed",
|
|
299
|
+
from: sourceRel,
|
|
300
|
+
to: "(trashed)",
|
|
301
|
+
direction: "trashed-orphan",
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
226
306
|
const state = await readMissionState(missionDir);
|
|
227
307
|
|
|
228
308
|
if (state === null) {
|
|
309
|
+
const hasContent = await hasNonCacheContent(missionDir);
|
|
229
310
|
skipped.push({
|
|
230
311
|
missionId,
|
|
231
312
|
dir: `${MISSIONS_DIR}/${missionId}`,
|
|
232
|
-
reason:
|
|
313
|
+
reason: hasContent
|
|
314
|
+
? "unreadable manifest — manually inspect or add mission.yaml"
|
|
315
|
+
: "empty remnant — use --clean-orphans to remove",
|
|
233
316
|
});
|
|
234
317
|
continue;
|
|
235
318
|
}
|
|
@@ -257,7 +340,6 @@ export async function runMissionArchive(
|
|
|
257
340
|
const targetDir = path.join(missionsPath, ARCHIVE_DIR_NAME, state);
|
|
258
341
|
const targetPath = path.join(targetDir, missionId);
|
|
259
342
|
const targetRel = `${MISSIONS_DIR}/${ARCHIVE_DIR_NAME}/${state}/${missionId}`;
|
|
260
|
-
const sourceRel = `${MISSIONS_DIR}/${missionId}`;
|
|
261
343
|
|
|
262
344
|
// RFC-0733: Check if mission directory is pinned before moving
|
|
263
345
|
// Gap fix: exempt intra-directory moves (dir stays within the same pinned parent)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
</MODULE_CONTRACT>
|
|
8
8
|
<CHANGE_SUMMARY>
|
|
9
9
|
<item>RFC-0573: initial forgeMissionModule registering mission.archive command.</item>
|
|
10
|
+
<item>RFC-0982: add --clean-orphans flag for trashing orphaned remnant directories.</item>
|
|
10
11
|
</CHANGE_SUMMARY>
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -42,6 +43,11 @@ export const forgeMissionModule: ForgeModule = {
|
|
|
42
43
|
kind: "string",
|
|
43
44
|
description: "Filter to a single terminal status (closed, aborted).",
|
|
44
45
|
},
|
|
46
|
+
"clean-orphans": {
|
|
47
|
+
kind: "boolean",
|
|
48
|
+
description:
|
|
49
|
+
"Trash orphaned directories (no mission.yaml, only cache files) instead of archiving them.",
|
|
50
|
+
},
|
|
45
51
|
},
|
|
46
52
|
execute: runMissionArchive,
|
|
47
53
|
});
|
package/os/mission/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ manifest state extraction, and archive result shapes.
|
|
|
10
10
|
</MODULE_CONTRACT>
|
|
11
11
|
<CHANGE_SUMMARY>
|
|
12
12
|
<item>RFC-0573: initial mission archive types.</item>
|
|
13
|
+
<item>RFC-0982: add "trashed-orphan" to MissionArchiveMove.direction.</item>
|
|
13
14
|
</CHANGE_SUMMARY>
|
|
14
15
|
*/
|
|
15
16
|
|
|
@@ -23,7 +24,7 @@ export interface MissionArchiveMove {
|
|
|
23
24
|
state: string;
|
|
24
25
|
from: string;
|
|
25
26
|
to: string;
|
|
26
|
-
direction: "into-archive" | "out-of-archive";
|
|
27
|
+
direction: "into-archive" | "out-of-archive" | "trashed-orphan";
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface MissionArchiveSkip {
|
package/package.json
CHANGED
|
@@ -66,12 +66,20 @@ Before any operator interaction, silently check whether the installed `@warpgogo
|
|
|
66
66
|
|
|
67
67
|
Read `PREFERENCES.md` at the project root. `forge create` writes a placeholder with `aiLanguage: en` and `documentationLanguage: en`.
|
|
68
68
|
|
|
69
|
-
Ask the operator:
|
|
69
|
+
Ask the operator two questions, one at a time:
|
|
70
|
+
|
|
71
|
+
**Question 1 — AI communication language:**
|
|
70
72
|
|
|
71
|
-
> In which language should the AI communicate with you? (e.g. en, ru, uk, de, es)
|
|
73
|
+
> In which language should the AI communicate with you? (e.g. en, ru, uk, de, es)
|
|
72
74
|
|
|
73
75
|
Accept free-form answers like "Russian", "русский", "uk" or "English". Prefer IETF BCP 47 language tags when the operator provides them.
|
|
74
76
|
|
|
77
|
+
**Question 2 — Documentation language:**
|
|
78
|
+
|
|
79
|
+
> In which language should project documentation be written? (RFCs, ADRs, READMEs) Press Enter to use English (default).
|
|
80
|
+
|
|
81
|
+
Accept free-form answers like "Russian", "русский", "uk" or "English". If the operator presses Enter or says "default" / "English", use `en`. Prefer IETF BCP 47 language tags when the operator provides them.
|
|
82
|
+
|
|
75
83
|
Write or merge the values into `PREFERENCES.md`. **All subsequent communication in this skill session uses the operator's chosen `aiLanguage`.**
|
|
76
84
|
|
|
77
85
|
If `PREFERENCES.md` already has non-default `aiLanguage` set (re-run of the skill), confirm the existing values with the operator instead of asking again. The operator may change them if desired.
|
package/src/onboarding/create.ts
CHANGED
|
@@ -397,7 +397,7 @@ Your Forge project is ready. The next step is mandatory: run \`/forge-bootstrap\
|
|
|
397
397
|
Run \`/forge-bootstrap\` now. It will ask you:
|
|
398
398
|
|
|
399
399
|
- Which language the AI should communicate with you in
|
|
400
|
-
- Which language project documentation should be written in
|
|
400
|
+
- Which language project documentation should be written in (defaults to English)
|
|
401
401
|
- Whether you prefer a business or creative working style
|
|
402
402
|
- Your name and how you want to be addressed
|
|
403
403
|
- Your stack configuration (or migrate an existing project)
|