@warpgogol/forge 5.1.3 → 5.2.3
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/AGENTS.md +3 -1
- package/bin/cli.ts +4 -0
- package/dist/bin/cli.js +4 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.js +1 -1
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/compass/compass.module.js +2 -2
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +3 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +87 -8
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/core/core.module.js +1 -1
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/mission/handlers/archive.d.ts.map +1 -1
- package/dist/os/mission/handlers/archive.js +22 -6
- package/dist/os/mission/handlers/archive.js.map +1 -1
- package/dist/os/program/lease.js +1 -1
- package/dist/os/program/lease.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts +4 -0
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -0
- package/dist/os/queue/handlers/queue-validate.js +62 -0
- package/dist/os/queue/handlers/queue-validate.js.map +1 -0
- package/dist/os/queue/index.d.ts +5 -0
- package/dist/os/queue/index.d.ts.map +1 -0
- package/dist/os/queue/index.js +18 -0
- package/dist/os/queue/index.js.map +1 -0
- package/dist/os/queue/manifest.d.ts +14 -0
- package/dist/os/queue/manifest.d.ts.map +1 -0
- package/dist/os/queue/manifest.js +110 -0
- package/dist/os/queue/manifest.js.map +1 -0
- package/dist/os/queue/queue.module.d.ts +3 -0
- package/dist/os/queue/queue.module.d.ts.map +1 -0
- package/dist/os/queue/queue.module.js +56 -0
- package/dist/os/queue/queue.module.js.map +1 -0
- package/dist/os/queue/types.d.ts +25 -0
- package/dist/os/queue/types.d.ts.map +1 -0
- package/dist/os/queue/types.js +28 -0
- package/dist/os/queue/types.js.map +1 -0
- package/dist/os/rfc/handlers/list-create.d.ts.map +1 -1
- package/dist/os/rfc/handlers/list-create.js +79 -32
- package/dist/os/rfc/handlers/list-create.js.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.d.ts +2 -7
- package/dist/os/rfc/handlers/pipeline-status.d.ts.map +1 -1
- package/dist/os/rfc/handlers/pipeline-status.js +15 -57
- package/dist/os/rfc/handlers/pipeline-status.js.map +1 -1
- package/dist/os/rfc/rfc.module.js +1 -1
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +2 -0
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/src/knowledge/index.d.ts +1 -0
- package/dist/src/knowledge/index.d.ts.map +1 -1
- package/dist/src/knowledge/index.js +1 -0
- package/dist/src/knowledge/index.js.map +1 -1
- package/dist/src/knowledge/sync.d.ts +24 -0
- package/dist/src/knowledge/sync.d.ts.map +1 -0
- package/dist/src/knowledge/sync.js +127 -0
- package/dist/src/knowledge/sync.js.map +1 -0
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +7 -6
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +15 -8
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/onboarding/upgrade.d.ts.map +1 -1
- package/dist/src/onboarding/upgrade.js +42 -13
- package/dist/src/onboarding/upgrade.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +62 -0
- package/dist/src/pipeline-status.d.ts.map +1 -0
- package/dist/src/pipeline-status.js +220 -0
- package/dist/src/pipeline-status.js.map +1 -0
- package/dist/src/types.d.ts +1 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/fs-atomic.js +1 -1
- package/dist/src/utils/fs-atomic.js.map +1 -1
- package/os/adr/adr.module.ts +1 -1
- package/os/compass/compass.module.ts +2 -2
- package/os/compass/handlers/compass-audit-handler.ts +116 -8
- package/os/compass/handlers/tests/compass-audit-plan.test.ts +98 -0
- package/os/compass/handlers/tests/compass-audit-record.test.ts +105 -0
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +133 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +124 -0
- package/os/core/core.module.ts +1 -1
- package/os/mission/handlers/archive.test.ts +49 -0
- package/os/mission/handlers/archive.ts +21 -6
- package/os/program/lease.ts +1 -1
- package/os/queue/handlers/queue-validate.ts +83 -0
- package/os/queue/index.ts +19 -0
- package/os/queue/manifest.ts +183 -0
- package/os/queue/queue-validate.test.ts +177 -0
- package/os/queue/queue.module.ts +59 -0
- package/os/queue/types.ts +49 -0
- package/os/rfc/handlers/list-create.ts +83 -35
- package/os/rfc/handlers/pipeline-status.ts +24 -67
- package/os/rfc/handlers/validate-rules.test.ts +50 -0
- package/os/rfc/rfc-create-concurrent.test.ts +118 -0
- package/os/rfc/rfc-create-hint.test.ts +90 -0
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +83 -0
- package/os/rfc/rfc.module.ts +1 -1
- package/os/spec/spec.module.ts +2 -0
- package/package.json +17 -1
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +46 -1
- package/src/knowledge/__tests__/sync.test.ts +227 -0
- package/src/knowledge/index.ts +7 -0
- package/src/knowledge/sync.ts +161 -0
- package/src/onboarding/doctor.ts +7 -6
- package/src/onboarding/init.ts +15 -8
- package/src/onboarding/upgrade.ts +47 -16
- package/src/pipeline-status.test.ts +192 -0
- package/src/pipeline-status.ts +319 -0
- package/src/tests/upgrade.test.ts +53 -0
- package/src/types.ts +2 -2
- package/src/utils/fs-atomic.ts +1 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Shared pipeline-status resolver (RFC-1140) — derives per-document
|
|
4
|
+
pipeline progress (audit, enhance, plan, implement) from document frontmatter
|
|
5
|
+
and pipeline artifacts. Single source of truth consumed by rfc.pipeline.status,
|
|
6
|
+
queue.validate, and the orchestrator's resume detection. Portable layer: no
|
|
7
|
+
os/ or kernel imports.</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Do not validate RFC/ADR content — use rfc.validate / adr.validate.</item>
|
|
10
|
+
<item>Do not mutate any files — derivation is read-only.</item>
|
|
11
|
+
<item>Do not store progress state — status is derived at read time, never persisted.</item>
|
|
12
|
+
</non-goals>
|
|
13
|
+
</MODULE_CONTRACT>
|
|
14
|
+
<CHANGE_SUMMARY>
|
|
15
|
+
<item>RFC-1140: initial shared resolver — artifact conventions (audit-*.md,
|
|
16
|
+
plan-*.md, enhancedAt, implementedAt) extracted from
|
|
17
|
+
os/rfc/handlers/pipeline-status.ts; QueueItemStatus derivation matrix added
|
|
18
|
+
for queue.validate.</item>
|
|
19
|
+
<item>RFC-1140: steps 1-4 — shared resolver, queue module, registration
|
|
20
|
+
|
|
21
|
+
Extract pipeline-status derivation into packages/forge/src/pipeline-status.ts, refactor rfc.pipeline.status onto it, add os/queue module with queue.validate command, register in WORKSHOP_MODULE_MAP.forge + bin/cli.ts + package.json exports.</item>
|
|
22
|
+
<item>RFC-1140: step 6 — cover shared resolver and queue.validate
|
|
23
|
+
|
|
24
|
+
Add pipeline-status.test.ts (derivation matrix, order, next selection, ADR, skipped) and queue-validate.test.ts (schema errors, unknown/dup ids, id-filename mismatch, empty items, dependsOn warning, JSON shape). Fix queue derivation: pipelineStep is the stage after the last completed stage, not first incomplete — handles non-contiguous artifacts.</item>
|
|
25
|
+
</CHANGE_SUMMARY>
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from "node:fs/promises";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import YAML from "yaml";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Pipeline stages (shared with rfc.pipeline.status)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export type PipelineStage = "audit" | "enhance" | "plan" | "implement";
|
|
37
|
+
|
|
38
|
+
export interface RfcPipelineStageInfo {
|
|
39
|
+
stage: PipelineStage;
|
|
40
|
+
done: boolean;
|
|
41
|
+
/** File or marker that confirms completion, when applicable. */
|
|
42
|
+
evidence?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const TERMINAL_STATUSES: ReadonlySet<string> = new Set([
|
|
46
|
+
"implemented",
|
|
47
|
+
"rejected",
|
|
48
|
+
"superseded",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Minimal frontmatter parsing (self-contained — src/ must not import os/)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export interface ParsedDocument {
|
|
56
|
+
frontmatter: Record<string, unknown>;
|
|
57
|
+
body: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseFrontmatter(source: string): ParsedDocument {
|
|
61
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
62
|
+
if (!match) {
|
|
63
|
+
return { frontmatter: {}, body: source };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
frontmatter: (YAML.parse(match[1]!) ?? {}) as Record<string, unknown>,
|
|
67
|
+
body: match[2] ?? "",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Artifact finders — the canonical conventions
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
export async function findAuditFile(
|
|
76
|
+
workspaceRoot: string,
|
|
77
|
+
docId: string,
|
|
78
|
+
): Promise<string | undefined> {
|
|
79
|
+
const auditsDir = path.join(workspaceRoot, "docs/audits");
|
|
80
|
+
try {
|
|
81
|
+
const entries = await fs.readdir(auditsDir);
|
|
82
|
+
const prefix = `audit-${docId.toLowerCase()}`;
|
|
83
|
+
const match = entries.find((e) => e.toLowerCase().startsWith(prefix) && e.endsWith(".md"));
|
|
84
|
+
return match ? path.join("docs/audits", match) : undefined;
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function findPlanFile(
|
|
91
|
+
workspaceRoot: string,
|
|
92
|
+
docId: string,
|
|
93
|
+
): Promise<string | undefined> {
|
|
94
|
+
const plansDir = path.join(workspaceRoot, "docs/plans");
|
|
95
|
+
try {
|
|
96
|
+
const entries = await fs.readdir(plansDir);
|
|
97
|
+
const prefix = `plan-${docId.toLowerCase()}`;
|
|
98
|
+
const match = entries.find((e) => e.toLowerCase().startsWith(prefix) && e.endsWith(".md"));
|
|
99
|
+
return match ? path.join("docs/plans", match) : undefined;
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Document resolution — active dir + archive (implemented docs may be archived)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export type QueueDocumentKind = "rfc" | "adr";
|
|
110
|
+
|
|
111
|
+
export interface ResolvedDocument {
|
|
112
|
+
id: string;
|
|
113
|
+
kind: QueueDocumentKind;
|
|
114
|
+
/** Workspace-relative path to the document file. */
|
|
115
|
+
file: string;
|
|
116
|
+
frontmatter: Record<string, unknown>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const DOC_ID_PATTERN = /^(RFC|ADR)-\d{4}$/i;
|
|
120
|
+
|
|
121
|
+
async function scanForDocument(
|
|
122
|
+
dirPath: string,
|
|
123
|
+
relativePrefix: string,
|
|
124
|
+
idLower: string,
|
|
125
|
+
): Promise<string | undefined> {
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
const found = await scanForDocument(path.join(dirPath, entry.name), relativePath, idLower);
|
|
136
|
+
if (found) return found;
|
|
137
|
+
} else if (
|
|
138
|
+
entry.isFile() &&
|
|
139
|
+
entry.name.endsWith(".md") &&
|
|
140
|
+
entry.name.toLowerCase().startsWith(idLower)
|
|
141
|
+
) {
|
|
142
|
+
return relativePath;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve an RFC-XXXX or ADR-XXXX id to its document file and parsed
|
|
150
|
+
* frontmatter. Scans the active dir and archive subdirectories recursively.
|
|
151
|
+
* Returns undefined when the id is malformed or no file matches.
|
|
152
|
+
*/
|
|
153
|
+
export async function resolveDocument(
|
|
154
|
+
workspaceRoot: string,
|
|
155
|
+
docId: string,
|
|
156
|
+
): Promise<ResolvedDocument | undefined> {
|
|
157
|
+
const idMatch = DOC_ID_PATTERN.exec(docId.trim());
|
|
158
|
+
if (!idMatch) return undefined;
|
|
159
|
+
const kind: QueueDocumentKind = idMatch[1]!.toUpperCase() === "ADR" ? "adr" : "rfc";
|
|
160
|
+
const baseDir = kind === "adr" ? "docs/adrs" : "docs/rfcs";
|
|
161
|
+
const idLower = docId.toLowerCase();
|
|
162
|
+
|
|
163
|
+
const found = await scanForDocument(path.join(workspaceRoot, baseDir), "", idLower);
|
|
164
|
+
if (!found) return undefined;
|
|
165
|
+
|
|
166
|
+
let source: string;
|
|
167
|
+
try {
|
|
168
|
+
source = await fs.readFile(path.join(workspaceRoot, baseDir, found), "utf-8");
|
|
169
|
+
} catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const parsed = parseFrontmatter(source);
|
|
173
|
+
return {
|
|
174
|
+
id: docId.toUpperCase(),
|
|
175
|
+
kind,
|
|
176
|
+
file: path.join(baseDir, found),
|
|
177
|
+
frontmatter: parsed.frontmatter,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// RFC stage derivation — shared by rfc.pipeline.status and queue.validate
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
export interface RfcStageInput {
|
|
186
|
+
status: string;
|
|
187
|
+
enhancedAt?: string;
|
|
188
|
+
implementedAt?: string;
|
|
189
|
+
auditFile?: string;
|
|
190
|
+
planFile?: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Compute the four pipeline stages for an RFC from frontmatter + artifacts. */
|
|
194
|
+
export function computeRfcPipelineStages(input: RfcStageInput): RfcPipelineStageInfo[] {
|
|
195
|
+
return [
|
|
196
|
+
{ stage: "audit", done: !!input.auditFile, evidence: input.auditFile },
|
|
197
|
+
{
|
|
198
|
+
stage: "enhance",
|
|
199
|
+
done: !!input.enhancedAt,
|
|
200
|
+
evidence: input.enhancedAt ? `enhancedAt: ${input.enhancedAt}` : undefined,
|
|
201
|
+
},
|
|
202
|
+
{ stage: "plan", done: !!input.planFile, evidence: input.planFile },
|
|
203
|
+
{
|
|
204
|
+
stage: "implement",
|
|
205
|
+
done: input.status === "implemented" || !!input.implementedAt,
|
|
206
|
+
evidence: input.implementedAt ? `implementedAt: ${input.implementedAt}` : undefined,
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** First incomplete stage for a non-terminal RFC, or null when terminal. */
|
|
212
|
+
export function nextPipelineStep(
|
|
213
|
+
status: string,
|
|
214
|
+
stages: RfcPipelineStageInfo[],
|
|
215
|
+
): PipelineStage | null {
|
|
216
|
+
if (TERMINAL_STATUSES.has(status)) return null;
|
|
217
|
+
for (const s of stages) {
|
|
218
|
+
if (!s.done) return s.stage;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Queue item derivation (RFC-1140)
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
export type QueueItemStatus = "pending" | "in-progress" | "implemented" | "skipped";
|
|
228
|
+
|
|
229
|
+
export interface QueueItemReport {
|
|
230
|
+
id: string;
|
|
231
|
+
status: QueueItemStatus;
|
|
232
|
+
/** First incomplete pipeline stage — present only when in-progress. */
|
|
233
|
+
pipelineStep?: PipelineStage;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Derive the queue status of a single document per the RFC-1140 matrix:
|
|
238
|
+
* implemented → implemented; rejected/superseded → skipped; RFC accepted or
|
|
239
|
+
* plan file → in-progress/implement; enhancedAt → in-progress/plan; audit
|
|
240
|
+
* file → in-progress/enhance; otherwise pending. ADR non-terminal → pending.
|
|
241
|
+
*/
|
|
242
|
+
export async function deriveQueueItemStatus(
|
|
243
|
+
workspaceRoot: string,
|
|
244
|
+
docId: string,
|
|
245
|
+
): Promise<QueueItemReport | undefined> {
|
|
246
|
+
const doc = await resolveDocument(workspaceRoot, docId);
|
|
247
|
+
if (!doc) return undefined;
|
|
248
|
+
|
|
249
|
+
const fm = doc.frontmatter;
|
|
250
|
+
const status = String(fm["status"] ?? "");
|
|
251
|
+
const implementedAt = fm["implementedAt"] ? String(fm["implementedAt"]) : undefined;
|
|
252
|
+
|
|
253
|
+
if (status === "implemented" || implementedAt) {
|
|
254
|
+
return { id: doc.id, status: "implemented" };
|
|
255
|
+
}
|
|
256
|
+
if (status === "rejected" || status === "superseded") {
|
|
257
|
+
return { id: doc.id, status: "skipped" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ADR pipeline is create → implement — no intermediate artifacts exist.
|
|
261
|
+
if (doc.kind === "adr") {
|
|
262
|
+
return { id: doc.id, status: "pending" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// RFC accepted means the plan step is done by definition.
|
|
266
|
+
if (status === "accepted") {
|
|
267
|
+
return { id: doc.id, status: "in-progress", pipelineStep: "implement" };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const enhancedAt = fm["enhancedAt"] ? String(fm["enhancedAt"]) : undefined;
|
|
271
|
+
const auditFile = await findAuditFile(workspaceRoot, doc.id);
|
|
272
|
+
const planFile = await findPlanFile(workspaceRoot, doc.id);
|
|
273
|
+
|
|
274
|
+
const stages = computeRfcPipelineStages({
|
|
275
|
+
status,
|
|
276
|
+
enhancedAt,
|
|
277
|
+
implementedAt,
|
|
278
|
+
auditFile,
|
|
279
|
+
planFile,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Queue semantics: the actionable step is the stage AFTER the last completed
|
|
283
|
+
// one — artifacts may be non-contiguous (e.g. enhancedAt without an audit
|
|
284
|
+
// file), where "first incomplete stage" would wrongly report audit.
|
|
285
|
+
let lastDone = -1;
|
|
286
|
+
for (let i = 0; i < stages.length; i++) {
|
|
287
|
+
if (stages[i]!.done) lastDone = i;
|
|
288
|
+
}
|
|
289
|
+
if (lastDone === -1) {
|
|
290
|
+
return { id: doc.id, status: "pending" };
|
|
291
|
+
}
|
|
292
|
+
const nextStage = stages[lastDone + 1];
|
|
293
|
+
if (!nextStage) {
|
|
294
|
+
return { id: doc.id, status: "pending" };
|
|
295
|
+
}
|
|
296
|
+
return { id: doc.id, status: "in-progress", pipelineStep: nextStage.stage };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface QueueReport {
|
|
300
|
+
items: QueueItemReport[];
|
|
301
|
+
/** First item whose derived status is pending or in-progress; null when fully terminal. */
|
|
302
|
+
next: string | null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Derive the full queue report, preserving manifest order. */
|
|
306
|
+
export async function deriveQueueReport(
|
|
307
|
+
workspaceRoot: string,
|
|
308
|
+
itemIds: string[],
|
|
309
|
+
): Promise<QueueReport> {
|
|
310
|
+
const items: QueueItemReport[] = [];
|
|
311
|
+
for (const id of itemIds) {
|
|
312
|
+
const report = await deriveQueueItemStatus(workspaceRoot, id);
|
|
313
|
+
// Unresolvable ids are reported by the manifest validator; the resolver
|
|
314
|
+
// skips them here so a partial report stays usable.
|
|
315
|
+
if (report) items.push(report);
|
|
316
|
+
}
|
|
317
|
+
const nextItem = items.find((i) => i.status === "pending" || i.status === "in-progress");
|
|
318
|
+
return { items, next: nextItem?.id ?? null };
|
|
319
|
+
}
|
|
@@ -431,3 +431,56 @@ test("forge.upgrade without --update-npm does not attempt npm install", async ()
|
|
|
431
431
|
expect(result.data?.npmUpdated).toBe(false);
|
|
432
432
|
expect(result.data?.npmUpdateSkipped).toBe(null);
|
|
433
433
|
});
|
|
434
|
+
|
|
435
|
+
function qaLog(entries: { id: string; title: string; body: string }[]): string {
|
|
436
|
+
const blocks = entries.map(
|
|
437
|
+
(e) =>
|
|
438
|
+
`### ${e.id}: ${e.title}\n\n\`\`\`knowledge-entry\nid: ${e.id}\nlayer: L0\ncreated: 2026-08-03\nstatus: active\n\`\`\`\n\n${e.body}\n`,
|
|
439
|
+
);
|
|
440
|
+
return `<!-- knowledge-layer: L0 -->\n\n# Q&A Log (L0)\n\n${blocks.join("\n")}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
test("forge.upgrade preserves project-accumulated knowledge entries (append-only merge)", async () => {
|
|
444
|
+
await setupForgeSource(tempDir, "0.2.0");
|
|
445
|
+
// Declare a knowledge file on the fixture skill
|
|
446
|
+
await writeFile(
|
|
447
|
+
join(tempDir, "packages", "forge", "skills", "fo", "fo-idea", "SKILL.md"),
|
|
448
|
+
"---\nname: fo-idea\nknowledge: [qa-log.md]\n---\n# fo-idea\n",
|
|
449
|
+
"utf8",
|
|
450
|
+
);
|
|
451
|
+
// Package ships K-0001 + new K-0003
|
|
452
|
+
await writeFile(
|
|
453
|
+
join(tempDir, "packages", "forge", "skills", "fo", "fo-idea", "qa-log.md"),
|
|
454
|
+
qaLog([
|
|
455
|
+
{ id: "K-0001", title: "First", body: "Package body." },
|
|
456
|
+
{ id: "K-0003", title: "Third", body: "New package entry." },
|
|
457
|
+
]),
|
|
458
|
+
"utf8",
|
|
459
|
+
);
|
|
460
|
+
await setupConsumerForgeYaml(tempDir, null);
|
|
461
|
+
|
|
462
|
+
// Local copy already accumulated: edited K-0001 + project-only K-0002
|
|
463
|
+
await mkdir(join(tempDir, ".agents", "skills", "fo-idea"), { recursive: true });
|
|
464
|
+
await writeFile(
|
|
465
|
+
join(tempDir, ".agents", "skills", "fo-idea", "qa-log.md"),
|
|
466
|
+
qaLog([
|
|
467
|
+
{ id: "K-0001", title: "First", body: "Locally edited body." },
|
|
468
|
+
{ id: "K-0002", title: "Local record", body: "Project-accumulated entry." },
|
|
469
|
+
]),
|
|
470
|
+
"utf8",
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const result = await runUpgrade({ argv: [], flags: {} }, makeContext(tempDir, false));
|
|
474
|
+
|
|
475
|
+
expect(result.data?.status).toBe("pass");
|
|
476
|
+
const merged = await readFile(join(tempDir, ".agents", "skills", "fo-idea", "qa-log.md"), "utf8");
|
|
477
|
+
// Local entries preserved — the data-loss bug would have wiped K-0002
|
|
478
|
+
expect(merged).toContain("K-0002");
|
|
479
|
+
expect(merged).toContain("Project-accumulated entry.");
|
|
480
|
+
// Local version of the conflicting entry wins
|
|
481
|
+
expect(merged).toContain("Locally edited body.");
|
|
482
|
+
expect(merged).not.toContain("Package body.");
|
|
483
|
+
// New package entry appended
|
|
484
|
+
expect(merged).toContain("K-0003");
|
|
485
|
+
expect(merged).toContain("New package entry.");
|
|
486
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
16
16
|
<item>RFC-1097: sweep — werkstatt-engine clean
|
|
17
17
|
|
|
18
18
|
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>
|
|
19
|
+
<item>RFC-1147: ForgeRegisteredCommandInfo.provider narrowed to "workspace" and siteName field removed — forge never enumerated sites; mirrors the KernelRegisteredCommandInfo cleanup.</item>
|
|
19
20
|
</CHANGE_SUMMARY>
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -112,8 +113,7 @@ export interface GateMetadata {
|
|
|
112
113
|
|
|
113
114
|
export interface ForgeRegisteredCommandInfo extends ForgeCommandMetadata {
|
|
114
115
|
name: string;
|
|
115
|
-
provider: "workspace"
|
|
116
|
-
siteName?: string;
|
|
116
|
+
provider: "workspace";
|
|
117
117
|
module?: string;
|
|
118
118
|
flags?: Record<string, ForgeFlagSpec>;
|
|
119
119
|
reads?: string[];
|
package/src/utils/fs-atomic.ts
CHANGED
|
@@ -56,7 +56,7 @@ function sleep(ms: number): Promise<void> {
|
|
|
56
56
|
function makeTempPath(filePath: string): string {
|
|
57
57
|
const dir = dirname(filePath);
|
|
58
58
|
const name = basename(filePath);
|
|
59
|
-
const random = randomBytes(6).toString("
|
|
59
|
+
const random = Array.from(randomBytes(6), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
60
60
|
return join(dir, `${name}.${random}.tmp`);
|
|
61
61
|
}
|
|
62
62
|
|