@warpgogol/forge 5.3.2 → 5.3.4
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/dist/os/compass/compass.module.js +1 -1
- package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-inventory.js +5 -2
- package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
- package/dist/os/compass/handlers/summary-record.d.ts +6 -1
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +14 -5
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.d.ts +18 -0
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.d.ts.map +1 -0
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.js +64 -0
- package/dist/os/compass/handlers/tests/forge-yaml-fixture.js.map +1 -0
- package/dist/os/compass/policy.d.ts +6 -1
- package/dist/os/compass/policy.d.ts.map +1 -1
- package/dist/os/compass/policy.js +8 -2
- package/dist/os/compass/policy.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +1 -2
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/rfc/handlers/validate.js +1 -1
- package/dist/src/config/forge-config.d.ts +21 -3
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +36 -7
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/create.d.ts.map +1 -1
- package/dist/src/onboarding/create.js +26 -10
- package/dist/src/onboarding/create.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +49 -2
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +24 -8
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +42 -15
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/os/compass/compass.module.ts +1 -1
- package/os/compass/handlers/compass-inventory.ts +6 -3
- package/os/compass/handlers/summary-record.ts +14 -5
- package/os/compass/handlers/tests/compass-policy.test.ts +15 -18
- package/os/compass/handlers/tests/compass-v2-contract.test.ts +38 -0
- package/os/compass/handlers/tests/forge-yaml-fixture.ts +69 -0
- package/os/compass/handlers/tests/summary-record.test.ts +25 -0
- package/os/compass/policy.ts +8 -2
- package/os/mission/handlers/archive.ts +1 -2
- package/os/rfc/handlers/validate.ts +1 -1
- package/package.json +1 -1
- package/skills/fo/fo-compass-annotate/SKILL.md +1 -1
- package/skills/fo/fo-design-summit/SKILL.md +3 -3
- package/skills/fo/fo-handoff/SKILL.md +2 -2
- package/skills/fo/fo-idea-audit/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
- package/skills/fo/fo-idea-implement/SKILL.md +4 -3
- package/skills/fo/fo-idea-plan/SKILL.md +16 -5
- package/skills/fo/fo-session-retro/SKILL.md +1 -1
- package/skills/fo/fo-session-save/SKILL.md +1 -1
- package/src/config/forge-config.ts +102 -57
- package/src/onboarding/create.ts +25 -9
- package/src/onboarding/doctor.ts +51 -2
- package/src/onboarding/upgrade.ts +36 -6
- package/src/tests/bindings-schema.test.ts +53 -1
- package/src/tests/doctor-bindings.test.ts +7 -2
- package/src/tests/doctor-skills-unmanaged.test.ts +145 -0
- package/src/tests/forge-config.test.ts +106 -2
- package/src/tests/skill-validate.test.ts +180 -0
- package/src/tests/upgrade.test.ts +60 -0
- package/src/validators/skill-validate.ts +43 -16
|
@@ -182,15 +182,26 @@ The grilling checks:
|
|
|
182
182
|
|
|
183
183
|
Address every concern the grilling raises by revising the draft. Do not persist until the grilling is satisfied.
|
|
184
184
|
|
|
185
|
-
### 5b. Summit
|
|
185
|
+
### 5b. Summit decision (autonomous)
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Decide yourself whether to run `fo-design-summit` before persisting the plan. **Do not ask the operator** — the planning agent holds the full context and is best placed to judge. The operator can always request a summit explicitly.
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
Eligibility — the RFC meets **any** of:
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
- `kind: architecture` AND `scope: workspace`
|
|
192
|
+
- `satisfies[]` includes 2+ DNA invariants
|
|
193
|
+
- Introduces a new package, command family, or lifecycle
|
|
194
|
+
- Supersedes an implemented RFC
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
Eligibility alone is not sufficient — most RFCs meet at least one criterion mechanically. Run the summit only when multi-perspective review is likely to surface findings that the audit, enhance, and grilling steps did not:
|
|
197
|
+
|
|
198
|
+
- The design holds genuinely contested trade-offs or more than one viable architecture.
|
|
199
|
+
- There is cross-cutting risk a single-perspective pass misses (security or privacy surface, migration of already-implemented behavior, multi-workspace blast radius).
|
|
200
|
+
- Step 3 or the grilling left material questions unresolved.
|
|
201
|
+
|
|
202
|
+
Skip the summit when the change is additive, follows an established pattern, or the plan converged cleanly — meeting a criterion only technically (e.g. listing 2 DNA invariants on a routine change) is a skip. When in doubt, skip: a missed summit is recoverable, operator attention is not.
|
|
203
|
+
|
|
204
|
+
If you run it, announce in one line (`Running design summit — <reason>`), invoke `fo-design-summit` via the `skill` tool, wait for it to complete, then proceed to step 6. If you skip, emit one line (`Design summit skipped — <reason>`) and proceed directly to step 6.
|
|
194
205
|
|
|
195
206
|
### 6. Persist the plan
|
|
196
207
|
|
|
@@ -450,7 +450,7 @@ If any operator insights were routed to `.agents/operator-profile.md`, offer the
|
|
|
450
450
|
|
|
451
451
|
Present a concise summary in `aiLanguage`. **ALL labels, headings, column names, and prose MUST be translated to `aiLanguage`** — the template below is structural only. Only identifiers (file paths, skill names, RFC/ADR ids, commit hashes) stay untranslated. This is non-negotiable: an English summary when `aiLanguage` is `ru` is a violation of this skill's language contract.
|
|
452
452
|
|
|
453
|
-
**Engineering Checkpoint
|
|
453
|
+
**Engineering Checkpoint:** The closing block follows the Engineering Checkpoint protocol defined in `_shared/fo-session-summary.md`. The agent MUST:
|
|
454
454
|
|
|
455
455
|
1. **Select mode** — analyze the session's changes. If the session changed architecture, runtime flow, state machines, persistence models, or public contracts, use **full mode** (6 sections). Otherwise, use **lightweight mode** (3 sections).
|
|
456
456
|
2. **Diagram selection** (full mode only) — apply the diagram selection rules table from `_shared/fo-session-summary.md` to determine if a Mermaid diagram is warranted. If no structural change warrants a diagram, explicitly state "No diagram: this session did not change system structure..."
|
|
@@ -60,7 +60,7 @@ Enhance saved session transcripts with semantic annotations, summaries, and qual
|
|
|
60
60
|
|
|
61
61
|
6. **Update the session file:**
|
|
62
62
|
- Update frontmatter: `summary`, `decisions`, refined `types`, corrected metadata.
|
|
63
|
-
- **Checkpoint fields
|
|
63
|
+
- **Checkpoint fields:** When the session produced substantial engineering work, populate the optional checkpoint frontmatter fields:
|
|
64
64
|
- `checkpoint` — object with `before`, `change`, `after` (each 1-2 sentences describing state N, what was done, state N+1)
|
|
65
65
|
- `systemDelta` — object with arrays: `changedContracts`, `changedSchemas`, `changedStateMachines`, `changedInvariants`, `changedPersistence`
|
|
66
66
|
- `diagrams` — array of objects: `type` (flowchart/sequenceDiagram/stateDiagram-v2/erDiagram/none), `scope` (current-system/session-delta), `caption`, `mermaid` (Mermaid syntax or empty string)
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
</non-goals>
|
|
7
7
|
</MODULE_CONTRACT>
|
|
8
8
|
<CHANGE_SUMMARY>
|
|
9
|
-
<item>RFC-0661: added optional knowledge.budgets binding for hot/warm layer character budget overrides.</item>
|
|
10
9
|
<item>RFC-0662: added optional knowledge.retentionDays and knowledge.staleDays bindings for compaction overrides.</item>
|
|
11
10
|
<item>RFC-0704: added optional independentVersionPackages field to forgeConfigSchema for packages with autonomous npm versions.</item>
|
|
12
11
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
@@ -15,14 +14,15 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
15
14
|
<item>RFC-1097: sweep — packages/forge + services clean
|
|
16
15
|
|
|
17
16
|
Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
|
|
18
|
-
<
|
|
17
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
18
|
+
<history>RFC-0391, RFC-0393, RFC-0537, RFC-0538, RFC-0539, RFC-0540, RFC-0543, RFC-0546, RFC-0639, RFC-0640, RFC-0643, RFC-0661, RFC-1224</history>
|
|
19
19
|
</CHANGE_SUMMARY>
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import * as fs from "../utils/sync-fs.ts";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { z } from "zod";
|
|
25
|
-
import { parse as parseYaml } from "yaml";
|
|
25
|
+
import { parse as parseYaml, parseDocument } from "yaml";
|
|
26
26
|
import {
|
|
27
27
|
TERMINOLOGY_DEFAULTS,
|
|
28
28
|
compassPolicyOverridesSchema,
|
|
@@ -36,22 +36,27 @@ import { listStackProfiles, type StackProfile } from "../profiles/stack-profile.
|
|
|
36
36
|
|
|
37
37
|
export const forgeBindingsSchema = z.object({
|
|
38
38
|
schema: z.literal("forge/bindings@1"),
|
|
39
|
-
commands: z
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
commands: z
|
|
40
|
+
.object({
|
|
41
|
+
validateRfc: z.string().nullable().default(null),
|
|
42
|
+
validateAdr: z.string().nullable().default(null),
|
|
43
|
+
implementStamp: z.string().nullable().default(null),
|
|
44
|
+
adrImplementStamp: z.string().nullable().default(null),
|
|
45
|
+
typecheck: z.string().nullable().default(null),
|
|
46
|
+
test: z.string().nullable().default(null),
|
|
47
|
+
scopedBuild: z.string().nullable().default(null),
|
|
48
|
+
specValidate: z.string().nullable().default(null),
|
|
49
|
+
sessionSave: z.string().nullable().default(null),
|
|
50
|
+
// Semantic keys (RFC-0639) — domain-neutral, optional
|
|
51
|
+
validate: z.string().nullable().default(null),
|
|
52
|
+
produce: z.string().nullable().default(null),
|
|
53
|
+
verify: z.string().nullable().default(null),
|
|
54
|
+
preview: z.string().nullable().default(null),
|
|
55
|
+
lint: z.string().nullable().default(null),
|
|
56
|
+
// RFC-1224: consumer-defined command keys (e.g. createRfc) stay in the
|
|
57
|
+
// parsed config so resolveBinding can resolve them.
|
|
58
|
+
})
|
|
59
|
+
.passthrough(),
|
|
55
60
|
paths: z.object({
|
|
56
61
|
invariantsFile: z.string().nullable().default(null),
|
|
57
62
|
compassDocs: z.array(z.string()).default([]),
|
|
@@ -98,6 +103,7 @@ export interface ForgeBindings {
|
|
|
98
103
|
validateRfc: string | null;
|
|
99
104
|
validateAdr: string | null;
|
|
100
105
|
implementStamp: string | null;
|
|
106
|
+
adrImplementStamp: string | null;
|
|
101
107
|
typecheck: string | null;
|
|
102
108
|
test: string | null;
|
|
103
109
|
scopedBuild: string | null;
|
|
@@ -177,44 +183,48 @@ export interface ForgeMigrationAdapter {
|
|
|
177
183
|
// Schema (forge/config@1)
|
|
178
184
|
// ---------------------------------------------------------------------------
|
|
179
185
|
|
|
180
|
-
export const forgeConfigSchema = z
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
186
|
+
export const forgeConfigSchema = z
|
|
187
|
+
.object({
|
|
188
|
+
schema: z.literal("forge/config@1"),
|
|
189
|
+
project: z.object({
|
|
190
|
+
name: z.string().min(1),
|
|
191
|
+
stack: z.array(z.string()).default([]),
|
|
192
|
+
packageManager: z.enum(["pnpm", "npm", "yarn", "bun", "none"]).default("pnpm"),
|
|
193
|
+
// RFC-0640: optional domain field — absent means software-domain fallback
|
|
194
|
+
domain: z.string().optional(),
|
|
195
|
+
}),
|
|
196
|
+
paths: z.object({
|
|
197
|
+
rfcsDir: z.string().default("docs/rfcs"),
|
|
198
|
+
adrsDir: z.string().default("docs/adrs"),
|
|
199
|
+
plansDir: z.string().default("docs/plans"),
|
|
200
|
+
auditsDir: z.string().default("docs/audits"),
|
|
201
|
+
specsDir: z.string().default("docs/specs"),
|
|
202
|
+
skillsDir: z.string().default(".agents/skills"),
|
|
203
|
+
sessionsDir: z.string().default("docs/sessions"),
|
|
204
|
+
}),
|
|
205
|
+
/** Bindings contract (RFC-0393). Optional — absent means no bindings. */
|
|
206
|
+
bindings: forgeBindingsSchema.optional(),
|
|
207
|
+
/** Skill packs (RFC-0539). Optional — absent means no project-local packs. */
|
|
208
|
+
skillPacks: z.array(forgeSkillPackSchema).optional(),
|
|
209
|
+
/** Migration adapters (RFC-0546). Optional — absent means built-in adapters only. */
|
|
210
|
+
migrationAdapters: z.array(forgeMigrationAdapterSchema).optional(),
|
|
211
|
+
/** Forge sync metadata (RFC-0543). Optional — absent means never synced. */
|
|
212
|
+
forge: z
|
|
213
|
+
.object({
|
|
214
|
+
syncedVersion: z.string().nullable().default(null),
|
|
215
|
+
})
|
|
216
|
+
.optional(),
|
|
217
|
+
/** RFC-0643: profile id — when present, loadForgeConfig loads the corresponding profiles/<id>.yaml */
|
|
218
|
+
// RFC-1118: accept the corrupted object form (whole StackProfile written by a
|
|
219
|
+
// previous buggy serialize) so loadForgeConfig can recover the declared id —
|
|
220
|
+
// a hard schema throw here would deadlock `forge upgrade`, the healing path.
|
|
221
|
+
profile: z.union([z.string(), z.object({ id: z.string().min(1) }).passthrough()]).optional(),
|
|
222
|
+
/** RFC-0704: packages with autonomous npm versions — ecosystem.commit skips platform bump when all staged files belong to these packages */
|
|
223
|
+
independentVersionPackages: z.array(z.string()).optional(),
|
|
224
|
+
// RFC-1224: consumer-owned top-level sections (e.g. `plugins:`) stay in the
|
|
225
|
+
// parsed config so full-serialization writes do not silently drop them.
|
|
226
|
+
})
|
|
227
|
+
.passthrough();
|
|
218
228
|
|
|
219
229
|
export interface ForgeConfig {
|
|
220
230
|
schema: "forge/config@1";
|
|
@@ -268,6 +278,10 @@ export const FORGE_CLI_BINDING_DEFAULTS: ForgeCliBindingDefault[] = [
|
|
|
268
278
|
key: "commands.implementStamp",
|
|
269
279
|
template: "forge rfc.implement.stamp --id {id} --implementation-commit {commit}",
|
|
270
280
|
},
|
|
281
|
+
{
|
|
282
|
+
key: "commands.adrImplementStamp",
|
|
283
|
+
template: "forge adr.implement.stamp --id {id} --implementation-commit {commit}",
|
|
284
|
+
},
|
|
271
285
|
{ key: "commands.specValidate", template: "forge spec.validate --spec={id} --json" },
|
|
272
286
|
{ key: "commands.sessionSave", template: "forge session.save --json" },
|
|
273
287
|
];
|
|
@@ -302,6 +316,7 @@ export function applyCliBindingDefaults(pm: string): ForgeBindings["commands"] {
|
|
|
302
316
|
validateRfc: null,
|
|
303
317
|
validateAdr: null,
|
|
304
318
|
implementStamp: null,
|
|
319
|
+
adrImplementStamp: null,
|
|
305
320
|
typecheck: null,
|
|
306
321
|
test: null,
|
|
307
322
|
scopedBuild: null,
|
|
@@ -471,6 +486,36 @@ export function serializeForgeConfig(config: ForgeConfig): Record<string, unknow
|
|
|
471
486
|
return out;
|
|
472
487
|
}
|
|
473
488
|
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// YAML document patching — RFC-1224
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* A single targeted write into forge.yaml: a dotted key path expressed as
|
|
495
|
+
* segments plus the value to set. `applyForgeYamlPatches` creates missing
|
|
496
|
+
* intermediate maps and preserves comments/formatting elsewhere in the file.
|
|
497
|
+
*/
|
|
498
|
+
export interface ForgeYamlPatch {
|
|
499
|
+
path: string[];
|
|
500
|
+
value: unknown;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Apply targeted patches to a forge.yaml document without rewriting it.
|
|
505
|
+
* Returns the patched YAML text, or null when the document cannot be parsed —
|
|
506
|
+
* callers should fall back to full serialization in that case.
|
|
507
|
+
*/
|
|
508
|
+
export function applyForgeYamlPatches(rawYaml: string, patches: ForgeYamlPatch[]): string | null {
|
|
509
|
+
const doc = parseDocument(rawYaml);
|
|
510
|
+
if (doc.errors.length > 0) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
for (const patch of patches) {
|
|
514
|
+
doc.setIn(patch.path, patch.value);
|
|
515
|
+
}
|
|
516
|
+
return doc.toString();
|
|
517
|
+
}
|
|
518
|
+
|
|
474
519
|
// ---------------------------------------------------------------------------
|
|
475
520
|
// Forge root resolver — the single place that decides monorepo vs npm-installed
|
|
476
521
|
// ---------------------------------------------------------------------------
|
package/src/onboarding/create.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
</non-goals>
|
|
10
10
|
</MODULE_CONTRACT>
|
|
11
11
|
<CHANGE_SUMMARY>
|
|
12
|
-
<item>RFC-0643: pass profileId to runInit so forge.yaml gets a `profile` field.</item>
|
|
13
12
|
<item>RFC-0664: scaffold memory layer (.agents/memory/) after init.</item>
|
|
14
13
|
<item>RFC-0877: in-place mode only — --in-place flag required, no subdirectory creation, name derived from folder, strict empty-directory check (only .git/ tolerated).</item>
|
|
15
14
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
@@ -18,7 +17,8 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
18
17
|
<item>RFC-1097: sweep — packages/forge + services clean
|
|
19
18
|
|
|
20
19
|
Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
|
|
21
|
-
<
|
|
20
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
21
|
+
<history>RFC-0544, RFC-0548, RFC-0550, RFC-0640, RFC-0643, RFC-1224</history>
|
|
22
22
|
</CHANGE_SUMMARY>
|
|
23
23
|
*/
|
|
24
24
|
|
|
@@ -32,6 +32,7 @@ import { scaffoldMemoryLayer } from "./memory-scaffold.ts";
|
|
|
32
32
|
import {
|
|
33
33
|
loadForgeConfig,
|
|
34
34
|
serializeForgeConfig,
|
|
35
|
+
applyForgeYamlPatches,
|
|
35
36
|
resolveForgeRoot,
|
|
36
37
|
resolvePackageManager,
|
|
37
38
|
applyCliBindingDefaults,
|
|
@@ -346,15 +347,30 @@ export async function runCreate(
|
|
|
346
347
|
const forgeYamlPath = path.join(targetDir, "forge.yaml");
|
|
347
348
|
if (await fio.exists(forgeYamlPath)) {
|
|
348
349
|
try {
|
|
349
|
-
const config = loadForgeConfig(targetDir);
|
|
350
350
|
const pm = resolvePackageManager(packageManager);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
const commandDefaults = applyCliBindingDefaults(pm);
|
|
352
|
+
// RFC-1224: patch the document init just wrote instead of re-serializing —
|
|
353
|
+
// preserves comments/formatting and any schema-unknown sections.
|
|
354
|
+
const raw = await fio.readFile(forgeYamlPath);
|
|
355
|
+
const patched = applyForgeYamlPatches(raw, [
|
|
356
|
+
{ path: ["project", "packageManager"], value: pm },
|
|
357
|
+
...Object.entries(commandDefaults).map(([key, value]) => ({
|
|
358
|
+
path: ["bindings", "commands", key],
|
|
359
|
+
value,
|
|
360
|
+
})),
|
|
361
|
+
]);
|
|
362
|
+
if (patched !== null) {
|
|
363
|
+
await fio.writeFile(forgeYamlPath, patched);
|
|
364
|
+
} else {
|
|
365
|
+
const config = loadForgeConfig(targetDir);
|
|
366
|
+
config.project.packageManager = pm;
|
|
367
|
+
if (config.bindings) {
|
|
368
|
+
config.bindings.commands = commandDefaults;
|
|
369
|
+
}
|
|
370
|
+
// RFC-1118: serializeForgeConfig writes the declared profile id verbatim —
|
|
371
|
+
// never the resolved StackProfile object, never drops an unresolvable id.
|
|
372
|
+
await fio.writeFile(forgeYamlPath, stringifyYaml(serializeForgeConfig(config)));
|
|
354
373
|
}
|
|
355
|
-
// RFC-1118: serializeForgeConfig writes the declared profile id verbatim —
|
|
356
|
-
// never the resolved StackProfile object, never drops an unresolvable id.
|
|
357
|
-
await fio.writeFile(forgeYamlPath, stringifyYaml(serializeForgeConfig(config)));
|
|
358
374
|
} catch {
|
|
359
375
|
// Post-processing is best-effort — init already wrote a valid config
|
|
360
376
|
}
|
package/src/onboarding/doctor.ts
CHANGED
|
@@ -8,7 +8,6 @@ Checks for forge.yaml, AGENTS.md, PREFERENCES.md, .agents/skills/, docs/rfcs/,
|
|
|
8
8
|
</non-goals>
|
|
9
9
|
</MODULE_CONTRACT>
|
|
10
10
|
<CHANGE_SUMMARY>
|
|
11
|
-
<item>RFC-0664: added memory-layer health check (budget usage, gitignore coverage, daily-file leak risk).</item>
|
|
12
11
|
<item>RFC-0704: added independent-version-packages check — validates that paths in independentVersionPackages exist and contain package.json.</item>
|
|
13
12
|
<item>RFC-0941: added pack-manifests advisory check — validates forge.plugin.yaml existence and schema for each declared skill pack.</item>
|
|
14
13
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
@@ -17,7 +16,9 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
17
16
|
<item>RFC-1097: sweep — packages/forge + services clean
|
|
18
17
|
|
|
19
18
|
Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
|
|
20
|
-
<
|
|
19
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
20
|
+
<item>RFC-1226: skills-unmanaged check — warn on SKILL.md-bearing dirs without .forge-managed marker</item>
|
|
21
|
+
<history>RFC-0391, RFC-0393, RFC-0524, RFC-0539, RFC-0540, RFC-0611, RFC-0640, RFC-0660, RFC-0661, RFC-0663, RFC-0664, RFC-0675, RFC-1224</history>
|
|
21
22
|
</CHANGE_SUMMARY>
|
|
22
23
|
*/
|
|
23
24
|
|
|
@@ -54,6 +55,7 @@ import { compactMemoryMd } from "./memory-compact.ts";
|
|
|
54
55
|
import { checkInvariants } from "./invariant-engine.ts";
|
|
55
56
|
import type { InvariantViolation } from "./invariant-engine.ts";
|
|
56
57
|
import { checkNpmToken, workshopNeedsWarpgogolToken } from "./npm-token-check.ts";
|
|
58
|
+
import { SKILL_MARKER_FILE } from "./skill-markers.ts";
|
|
57
59
|
import { execSync } from "../utils/sync-fs.ts";
|
|
58
60
|
import * as fs from "../utils/sync-fs.ts";
|
|
59
61
|
import type { ProfilePrerequisite } from "../profiles/profile-schema.ts";
|
|
@@ -142,6 +144,7 @@ const BINDING_COMMAND_KEYS = [
|
|
|
142
144
|
"commands.validateRfc",
|
|
143
145
|
"commands.validateAdr",
|
|
144
146
|
"commands.implementStamp",
|
|
147
|
+
"commands.adrImplementStamp",
|
|
145
148
|
"commands.typecheck",
|
|
146
149
|
"commands.test",
|
|
147
150
|
"commands.scopedBuild",
|
|
@@ -762,6 +765,49 @@ async function checkPackSkills(workspaceRoot: string, io: WorkspaceIO): Promise<
|
|
|
762
765
|
};
|
|
763
766
|
}
|
|
764
767
|
|
|
768
|
+
// ---------------------------------------------------------------------------
|
|
769
|
+
// Unmanaged skill directories (RFC-1226)
|
|
770
|
+
// ---------------------------------------------------------------------------
|
|
771
|
+
|
|
772
|
+
// Visibility only: a dir bearing SKILL.md without .forge-managed is
|
|
773
|
+
// consumer-owned per RFC-1154 — report it, never prune or block.
|
|
774
|
+
async function checkUnmanagedSkillDirs(workspaceRoot: string, io: WorkspaceIO): Promise<DoctorCheck> {
|
|
775
|
+
let config;
|
|
776
|
+
try {
|
|
777
|
+
config = loadForgeConfig(workspaceRoot);
|
|
778
|
+
} catch {
|
|
779
|
+
return { name: "skills-unmanaged", status: "pass", message: "No forge.yaml — unmanaged skill dir check skipped" };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const skillsDir = join(workspaceRoot, config.paths.skillsDir);
|
|
783
|
+
let entries;
|
|
784
|
+
try {
|
|
785
|
+
entries = await io.readdir(skillsDir);
|
|
786
|
+
} catch {
|
|
787
|
+
return { name: "skills-unmanaged", status: "pass", message: "skillsDir absent — nothing to scan" };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const unmanaged: string[] = [];
|
|
791
|
+
for (const entry of entries) {
|
|
792
|
+
if (!entry.isDirectory) continue;
|
|
793
|
+
const dir = join(skillsDir, entry.name);
|
|
794
|
+
// Support dirs (_shared/, knowledge-only dirs) are not skills — skip.
|
|
795
|
+
if (!(await io.exists(join(dir, "SKILL.md")))) continue;
|
|
796
|
+
if (await io.exists(join(dir, SKILL_MARKER_FILE))) continue;
|
|
797
|
+
unmanaged.push(entry.name);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (unmanaged.length === 0) {
|
|
801
|
+
return { name: "skills-unmanaged", status: "pass", message: "all skill dirs forge-managed" };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
name: "skills-unmanaged",
|
|
806
|
+
status: "warn",
|
|
807
|
+
message: `${unmanaged.length} unmanaged skill dir(s): ${unmanaged.join(", ")} — consumer-owned (no ${SKILL_MARKER_FILE} marker). Port into a declared skillPacks pack to make them forge-managed, or leave as-is.`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
765
811
|
// ---------------------------------------------------------------------------
|
|
766
812
|
// Pack manifest diagnostics (RFC-0941)
|
|
767
813
|
// ---------------------------------------------------------------------------
|
|
@@ -1455,6 +1501,9 @@ export async function runDoctor(
|
|
|
1455
1501
|
const packManifestCheck = await checkPackManifests(workspaceRoot, fio);
|
|
1456
1502
|
checks.push(packManifestCheck);
|
|
1457
1503
|
|
|
1504
|
+
// RFC-1226: List unmanaged skill dirs under skillsDir (visibility only)
|
|
1505
|
+
checks.push(await checkUnmanagedSkillDirs(workspaceRoot, fio));
|
|
1506
|
+
|
|
1458
1507
|
// RFC-0704: Check independent version packages — paths must exist and contain package.json
|
|
1459
1508
|
const independentCheck = await checkIndependentVersionPackages(workspaceRoot, fio);
|
|
1460
1509
|
checks.push(independentCheck);
|
|
@@ -11,7 +11,6 @@ With --update-npm, also updates @warpgogol/forge from npm before syncing (skippe
|
|
|
11
11
|
</non-goals>
|
|
12
12
|
</MODULE_CONTRACT>
|
|
13
13
|
<CHANGE_SUMMARY>
|
|
14
|
-
<item>RFC-0611: added nested AGENTS.md generation after skill sync.</item>
|
|
15
14
|
<item>RFC-0663: added syncSharedKnowledge step to sync shared knowledge layer to .agents/skills/shared-knowledge/.</item>
|
|
16
15
|
<item>RFC-0664: added scaffoldMemoryLayer step to scaffold .agents/memory/ and .gitignore block.</item>
|
|
17
16
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
@@ -20,7 +19,8 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
20
19
|
<item>RFC-1097: sweep — werkstatt-engine clean
|
|
21
20
|
|
|
22
21
|
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.</item>
|
|
23
|
-
<
|
|
22
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
23
|
+
<history>RFC-0543, RFC-0611, RFC-1224</history>
|
|
24
24
|
</CHANGE_SUMMARY>
|
|
25
25
|
*/
|
|
26
26
|
|
|
@@ -35,10 +35,12 @@ import {
|
|
|
35
35
|
resolveForgeRoot,
|
|
36
36
|
loadForgeConfig,
|
|
37
37
|
serializeForgeConfig,
|
|
38
|
+
applyForgeYamlPatches,
|
|
38
39
|
resolvePmRunner,
|
|
39
40
|
resolvePmInstall,
|
|
40
41
|
type ForgeConfig,
|
|
41
42
|
type ForgeBindings,
|
|
43
|
+
type ForgeYamlPatch,
|
|
42
44
|
} from "../config/forge-config.ts";
|
|
43
45
|
import { FORGE_SKILLS, discoverPackSkills } from "../registry.ts";
|
|
44
46
|
import { syncKnowledgeFile } from "../knowledge/index.ts";
|
|
@@ -452,18 +454,38 @@ async function updateSyncedVersion(
|
|
|
452
454
|
workspaceRoot: string,
|
|
453
455
|
config: ForgeConfig,
|
|
454
456
|
version: string,
|
|
457
|
+
bindingsAdded: { key: string; value: string }[],
|
|
455
458
|
dryRun: boolean,
|
|
456
459
|
io: WorkspaceIO,
|
|
460
|
+
warn: (msg: string) => void,
|
|
457
461
|
): Promise<void> {
|
|
458
462
|
if (dryRun) return;
|
|
459
463
|
|
|
460
|
-
//
|
|
464
|
+
// Keep the in-memory config truthful for downstream steps (doctor).
|
|
461
465
|
config.forge = { syncedVersion: version };
|
|
462
466
|
|
|
463
|
-
//
|
|
467
|
+
// RFC-1224: patch the existing document — syncedVersion plus newly added
|
|
468
|
+
// binding defaults — instead of re-serializing, so consumer-owned keys,
|
|
469
|
+
// sections, comments and ordering survive the rewrite.
|
|
470
|
+
const patches: ForgeYamlPatch[] = [
|
|
471
|
+
{ path: ["forge", "syncedVersion"], value: version },
|
|
472
|
+
...bindingsAdded.map((b) => ({
|
|
473
|
+
path: ["bindings", ...b.key.split(".")],
|
|
474
|
+
value: b.value,
|
|
475
|
+
})),
|
|
476
|
+
];
|
|
477
|
+
const forgeYamlPath = path.join(workspaceRoot, "forge.yaml");
|
|
478
|
+
const raw = await io.readFile(forgeYamlPath).catch(() => null);
|
|
479
|
+
const patched = raw === null ? null : applyForgeYamlPatches(raw, patches);
|
|
480
|
+
if (patched !== null) {
|
|
481
|
+
await io.writeFile(forgeYamlPath, patched);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
warn(
|
|
485
|
+
"forge.upgrade: forge.yaml patch failed — falling back to full serialization (schema-unknown keys may be dropped)",
|
|
486
|
+
);
|
|
464
487
|
// RFC-1118: serializeForgeConfig writes the declared profile id verbatim —
|
|
465
488
|
// never the resolved StackProfile object, never drops an unresolvable id.
|
|
466
|
-
const forgeYamlPath = path.join(workspaceRoot, "forge.yaml");
|
|
467
489
|
await io.writeFile(forgeYamlPath, stringifyYaml(serializeForgeConfig(config)));
|
|
468
490
|
}
|
|
469
491
|
|
|
@@ -689,7 +711,15 @@ export async function runUpgrade(
|
|
|
689
711
|
|
|
690
712
|
// Step 5: Update forge.syncedVersion
|
|
691
713
|
if (!isDryRun) {
|
|
692
|
-
await updateSyncedVersion(
|
|
714
|
+
await updateSyncedVersion(
|
|
715
|
+
workspaceRoot,
|
|
716
|
+
config,
|
|
717
|
+
toVersion,
|
|
718
|
+
bindingsAdded,
|
|
719
|
+
false,
|
|
720
|
+
fio,
|
|
721
|
+
(msg) => context.logger.warn(msg),
|
|
722
|
+
);
|
|
693
723
|
}
|
|
694
724
|
|
|
695
725
|
// Step 5b: Generate nested AGENTS.md (RFC-0611). RFC-1153: the write path
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
</MODULE_CONTRACT>
|
|
8
8
|
<CHANGE_SUMMARY>
|
|
9
9
|
<item>RFC-0639: initial test suite for semantic command keys, terminology resolution, and applyCliBindingDefaults.</item>
|
|
10
|
+
<item>RFC-1224: adrImplementStamp schema/default coverage; consumer-defined command key passthrough.</item>
|
|
11
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
10
12
|
</CHANGE_SUMMARY>
|
|
11
13
|
*/
|
|
12
14
|
|
|
@@ -174,12 +176,14 @@ describe("RFC-0639: Terminology schema change", () => {
|
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
describe("RFC-0639: applyCliBindingDefaults", () => {
|
|
177
|
-
test("returns
|
|
179
|
+
test("returns 14 keys with semantic keys as null", () => {
|
|
178
180
|
const commands = applyCliBindingDefaults("pnpm");
|
|
179
181
|
// CLI-backed keys are non-null
|
|
180
182
|
expect(commands.validateRfc).not.toBeNull();
|
|
181
183
|
expect(commands.validateAdr).not.toBeNull();
|
|
182
184
|
expect(commands.implementStamp).not.toBeNull();
|
|
185
|
+
// RFC-1224: adr.implement.stamp promoted to a first-class default binding
|
|
186
|
+
expect(commands.adrImplementStamp).not.toBeNull();
|
|
183
187
|
expect(commands.specValidate).not.toBeNull();
|
|
184
188
|
expect(commands.sessionSave).not.toBeNull();
|
|
185
189
|
// Software-domain keys are null
|
|
@@ -312,3 +316,51 @@ describe("RFC-0639: resolveTerminology", () => {
|
|
|
312
316
|
expect(resolveTerminology(config, undefined, "source")).toBe("source file");
|
|
313
317
|
});
|
|
314
318
|
});
|
|
319
|
+
|
|
320
|
+
describe("RFC-1224: promoted + consumer-defined command keys", () => {
|
|
321
|
+
test("forgeBindingsSchema accepts adrImplementStamp", () => {
|
|
322
|
+
const result = forgeBindingsSchema.safeParse({
|
|
323
|
+
schema: "forge/bindings@1",
|
|
324
|
+
commands: {
|
|
325
|
+
adrImplementStamp:
|
|
326
|
+
"pnpm exec forge adr.implement.stamp --id {id} --implementation-commit {commit}",
|
|
327
|
+
},
|
|
328
|
+
paths: {
|
|
329
|
+
invariantsFile: null,
|
|
330
|
+
compassDocs: [],
|
|
331
|
+
reviewsDir: null,
|
|
332
|
+
handoffsDir: null,
|
|
333
|
+
sessionsDir: null,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
expect(result.success).toBe(true);
|
|
337
|
+
if (result.success) {
|
|
338
|
+
expect(result.data.commands.adrImplementStamp).toBe(
|
|
339
|
+
"pnpm exec forge adr.implement.stamp --id {id} --implementation-commit {commit}",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("forgeBindingsSchema retains consumer-defined command keys", () => {
|
|
345
|
+
const result = forgeBindingsSchema.safeParse({
|
|
346
|
+
schema: "forge/bindings@1",
|
|
347
|
+
commands: {
|
|
348
|
+
validateRfc: null,
|
|
349
|
+
createRfc: "pnpm exec forge rfc.create",
|
|
350
|
+
},
|
|
351
|
+
paths: {
|
|
352
|
+
invariantsFile: null,
|
|
353
|
+
compassDocs: [],
|
|
354
|
+
reviewsDir: null,
|
|
355
|
+
handoffsDir: null,
|
|
356
|
+
sessionsDir: null,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
expect(result.success).toBe(true);
|
|
360
|
+
if (result.success) {
|
|
361
|
+
expect((result.data.commands as Record<string, unknown>)["createRfc"]).toBe(
|
|
362
|
+
"pnpm exec forge rfc.create",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
</MODULE_CONTRACT>
|
|
5
5
|
<CHANGE_SUMMARY>
|
|
6
6
|
<item>RFC-0540: initial doctor binding notices tests.</item>
|
|
7
|
+
<item>RFC-1224: adrImplementStamp added to notice inventory and fixtures.</item>
|
|
8
|
+
<item>RFC-1224: preserve operator forge.yaml content on upgrade, promote adrImplementStamp binding</item>
|
|
7
9
|
</CHANGE_SUMMARY>
|
|
8
10
|
*/
|
|
9
11
|
|
|
@@ -58,7 +60,7 @@ async function setupMinimalProject(dir: string): Promise<void> {
|
|
|
58
60
|
await writeFile(join(dir, "PREFERENCES.md"), "# Test", "utf8");
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
test("doctor emits defaultable-binding-null notices for all
|
|
63
|
+
test("doctor emits defaultable-binding-null notices for all 6 forge-CLI bindings when null", async () => {
|
|
62
64
|
await setupMinimalProject(tempDir);
|
|
63
65
|
await writeForgeYaml(
|
|
64
66
|
tempDir,
|
|
@@ -96,11 +98,12 @@ bindings:
|
|
|
96
98
|
|
|
97
99
|
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
98
100
|
const notices = result.data?.bindings?.notices ?? [];
|
|
99
|
-
expect(notices).toHaveLength(
|
|
101
|
+
expect(notices).toHaveLength(6);
|
|
100
102
|
const keys = notices.map((n) => n.key);
|
|
101
103
|
expect(keys).toContain("commands.validateRfc");
|
|
102
104
|
expect(keys).toContain("commands.validateAdr");
|
|
103
105
|
expect(keys).toContain("commands.implementStamp");
|
|
106
|
+
expect(keys).toContain("commands.adrImplementStamp");
|
|
104
107
|
expect(keys).toContain("commands.specValidate");
|
|
105
108
|
expect(keys).toContain("commands.sessionSave");
|
|
106
109
|
for (const notice of notices) {
|
|
@@ -131,6 +134,7 @@ bindings:
|
|
|
131
134
|
validateRfc: "custom rfc validate command"
|
|
132
135
|
validateAdr: "custom adr validate command"
|
|
133
136
|
implementStamp: "custom stamp command"
|
|
137
|
+
adrImplementStamp: "custom adr stamp command"
|
|
134
138
|
typecheck: null
|
|
135
139
|
test: null
|
|
136
140
|
scopedBuild: null
|
|
@@ -172,6 +176,7 @@ bindings:
|
|
|
172
176
|
validateRfc: "npx forge rfc.validate --id {id} --json"
|
|
173
177
|
validateAdr: "npx forge adr.validate --id {id} --json"
|
|
174
178
|
implementStamp: "npx forge rfc.implement.stamp --id {id} --implementation-commit {commit}"
|
|
179
|
+
adrImplementStamp: "npx forge adr.implement.stamp --id {id} --implementation-commit {commit}"
|
|
175
180
|
typecheck: null
|
|
176
181
|
test: null
|
|
177
182
|
scopedBuild: null
|