@minhduydev/mdpi 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/template/.pi/VERSION +1 -1
- package/dist/template/.pi/extensions/templates-injector.ts +35 -7
- package/dist/template/.pi/prompts/INDEX.md +3 -9
- package/dist/template/.pi/skills/INDEX.md +39 -8
- package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
- package/dist/template/.pi/skills/frontend-design/SKILL.md +1 -1
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
- package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
- package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
- package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
- package/dist/template/.pi/skills/react-best-practices/SKILL.md +79 -1
- package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
- package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
- package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
- package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
- package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -0
- package/dist/template/.pi/skills/v0/SKILL.md +264 -0
- package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
- package/package.json +1 -1
- package/dist/template/.pi/prompts/loop-check.md +0 -87
- package/dist/template/.pi/prompts/loop-init.md +0 -157
- package/dist/template/.pi/prompts/loop-review.md +0 -90
- package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
- package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
- package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
- package/dist/template/.pi/templates/loop-github-action.yml +0 -162
- package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
- package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
- package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
- package/dist/template/.pi/templates/loop-state.json +0 -24
- package/dist/template/.pi/templates/loop-state.md +0 -98
- package/dist/template/.pi/templates/loop-vision.md +0 -110
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loop-orchestrator.test.ts — pure-helper TDD for the Node SDK orchestrator (T9).
|
|
3
|
-
*
|
|
4
|
-
* Covers the pure, side-effect-free helpers only. The runtime smoke (real
|
|
5
|
-
* worktree + real pi + real gh on a throwaway repo) is deferred to T15
|
|
6
|
-
* (supervised checkpoint). Run with: `npx tsx --test .pi/templates/loop-orchestrator.test.ts`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { test } from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
parseGateCommand,
|
|
14
|
-
buildMakerPrompt,
|
|
15
|
-
nextItemId,
|
|
16
|
-
updateStateJson,
|
|
17
|
-
isAlreadyProcessed,
|
|
18
|
-
auditShipToolCalls,
|
|
19
|
-
enforceBudgetCap,
|
|
20
|
-
accumulateUsage,
|
|
21
|
-
MAKER_TOOLS,
|
|
22
|
-
} from "./loop-orchestrator.ts";
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Sample fixtures
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
const VISION_WITH_GATE = `# Loop Vision
|
|
29
|
-
|
|
30
|
-
## Goal
|
|
31
|
-
Do the thing.
|
|
32
|
-
|
|
33
|
-
## Scope
|
|
34
|
-
- edit src/
|
|
35
|
-
|
|
36
|
-
## Gate
|
|
37
|
-
|
|
38
|
-
Command (exit 0 = pass):
|
|
39
|
-
|
|
40
|
-
\`\`\`bash
|
|
41
|
-
npm test
|
|
42
|
-
\`\`\`
|
|
43
|
-
|
|
44
|
-
**Pass:** ship.
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
const VISION_NO_GATE_HEADING = `# Loop Vision
|
|
48
|
-
|
|
49
|
-
## Goal
|
|
50
|
-
Do the thing.
|
|
51
|
-
|
|
52
|
-
\`\`\`bash
|
|
53
|
-
npm test
|
|
54
|
-
\`\`\`
|
|
55
|
-
`;
|
|
56
|
-
|
|
57
|
-
const VISION_GATE_EMPTY_BLOCK = `# Loop Vision
|
|
58
|
-
|
|
59
|
-
## Gate
|
|
60
|
-
|
|
61
|
-
\`\`\`bash
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
-
`;
|
|
66
|
-
|
|
67
|
-
const VISION_GATE_MULTIPLE_BLOCKS = `# Loop Vision
|
|
68
|
-
|
|
69
|
-
## Gate
|
|
70
|
-
|
|
71
|
-
\`\`\`bash
|
|
72
|
-
npm test
|
|
73
|
-
\`\`\`
|
|
74
|
-
|
|
75
|
-
\`\`\`bash
|
|
76
|
-
npm run lint
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
end
|
|
80
|
-
`;
|
|
81
|
-
|
|
82
|
-
const VISION_GATE_UNTERMINATED = `# Loop Vision
|
|
83
|
-
|
|
84
|
-
## Gate
|
|
85
|
-
|
|
86
|
-
\`\`\`bash
|
|
87
|
-
npm test
|
|
88
|
-
`;
|
|
89
|
-
|
|
90
|
-
const VISION_GATE_NEXT_HEADING_ENDS = `# Loop Vision
|
|
91
|
-
|
|
92
|
-
## Gate
|
|
93
|
-
|
|
94
|
-
\`\`\`bash
|
|
95
|
-
npm test
|
|
96
|
-
\`\`\`
|
|
97
|
-
|
|
98
|
-
## Scope
|
|
99
|
-
- edit src/
|
|
100
|
-
`;
|
|
101
|
-
|
|
102
|
-
const STATE = {
|
|
103
|
-
loop_name: "ci-triage",
|
|
104
|
-
owner: "ops",
|
|
105
|
-
cadence: "manual",
|
|
106
|
-
last_run: null,
|
|
107
|
-
in_progress: ["item-9"],
|
|
108
|
-
completed: [],
|
|
109
|
-
escalated: [],
|
|
110
|
-
failures: [],
|
|
111
|
-
lessons: [],
|
|
112
|
-
processed: ["item-1", "item-2"],
|
|
113
|
-
stop_conditions_met: [],
|
|
114
|
-
metrics: {
|
|
115
|
-
runs: 0,
|
|
116
|
-
killed: false,
|
|
117
|
-
kill_reason: null,
|
|
118
|
-
tokens_used: 0,
|
|
119
|
-
token_cap: null,
|
|
120
|
-
pr_opened: 0,
|
|
121
|
-
items_fixed: 0,
|
|
122
|
-
items_skipped: 0,
|
|
123
|
-
items_escalated: 0,
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// parseGateCommand
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
test("parseGateCommand: extracts first ```bash under ## Gate", () => {
|
|
132
|
-
assert.equal(parseGateCommand(VISION_WITH_GATE), "npm test");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("parseGateCommand: trims surrounding whitespace per line", () => {
|
|
136
|
-
const md = `## Gate\n\n\`\`\`bash\n npm test \n npm run lint \n\`\`\`\n`;
|
|
137
|
-
assert.equal(parseGateCommand(md), "npm test\nnpm run lint");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("parseGateCommand: section ends at next level-2 heading (still returns the one block)", () => {
|
|
141
|
-
assert.equal(parseGateCommand(VISION_GATE_NEXT_HEADING_ENDS), "npm test");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("parseGateCommand: returns null when ## Gate heading is missing", () => {
|
|
145
|
-
assert.equal(parseGateCommand(VISION_NO_GATE_HEADING), null);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("parseGateCommand: returns null when the bash block is empty", () => {
|
|
149
|
-
assert.equal(parseGateCommand(VISION_GATE_EMPTY_BLOCK), null);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("parseGateCommand: returns null when multiple bash blocks under ## Gate", () => {
|
|
153
|
-
assert.equal(parseGateCommand(VISION_GATE_MULTIPLE_BLOCKS), null);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("parseGateCommand: returns null when the block is unterminated", () => {
|
|
157
|
-
assert.equal(parseGateCommand(VISION_GATE_UNTERMINATED), null);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("parseGateCommand: returns null for empty input", () => {
|
|
161
|
-
assert.equal(parseGateCommand(""), null);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
// buildMakerPrompt
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
|
|
168
|
-
test("buildMakerPrompt: contains the loop name", () => {
|
|
169
|
-
const p = buildMakerPrompt("ci-triage", VISION_WITH_GATE, STATE);
|
|
170
|
-
assert.match(p, /ci-triage/);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("buildMakerPrompt: tells the maker it cannot ship", () => {
|
|
174
|
-
const p = buildMakerPrompt("ci-triage", VISION_WITH_GATE, STATE);
|
|
175
|
-
assert.match(p, /cannot ship|CANNOT SHIP|do not.*push/i);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("buildMakerPrompt: references VISION.md", () => {
|
|
179
|
-
const p = buildMakerPrompt("ci-triage", VISION_WITH_GATE, STATE);
|
|
180
|
-
assert.match(p, /VISION\.md/);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// nextItemId
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
test("nextItemId: returns the first in_progress item when present", () => {
|
|
188
|
-
assert.equal(nextItemId(STATE), "item-9");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("nextItemId: falls back to provided fallback when in_progress is empty", () => {
|
|
192
|
-
const s = { ...STATE, in_progress: [] };
|
|
193
|
-
assert.equal(nextItemId(s, "manual-42"), "manual-42");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// updateStateJson
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
test("updateStateJson: applies a shallow top-level patch", () => {
|
|
201
|
-
const next = updateStateJson(STATE, { last_run: "2026-01-01T00:00:00Z" });
|
|
202
|
-
assert.equal(next.last_run, "2026-01-01T00:00:00Z");
|
|
203
|
-
// immutability: original untouched
|
|
204
|
-
assert.equal(STATE.last_run, null);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("updateStateJson: shallow-merges metrics", () => {
|
|
208
|
-
const next = updateStateJson(STATE, { metrics: { ...STATE.metrics, runs: 5 } });
|
|
209
|
-
assert.equal(next.metrics.runs, 5);
|
|
210
|
-
assert.equal(next.metrics.items_fixed, 0);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
// isAlreadyProcessed
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
|
|
217
|
-
test("isAlreadyProcessed: true for a processed item", () => {
|
|
218
|
-
assert.equal(isAlreadyProcessed(STATE, "item-1"), true);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("isAlreadyProcessed: false for an unprocessed item", () => {
|
|
222
|
-
assert.equal(isAlreadyProcessed(STATE, "item-99"), false);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test("isAlreadyProcessed: false when processed is missing", () => {
|
|
226
|
-
const s = { ...STATE, processed: undefined as unknown as string[] };
|
|
227
|
-
assert.equal(isAlreadyProcessed(s, "item-1"), false);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
// auditShipToolCalls
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
|
|
234
|
-
test("auditShipToolCalls: ok for maker-only tools", () => {
|
|
235
|
-
assert.deepEqual(auditShipToolCalls(["bash", "edit", "read", "grep", "find", "write"]), {
|
|
236
|
-
ok: true,
|
|
237
|
-
offenders: [],
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("auditShipToolCalls: flags push", () => {
|
|
242
|
-
assert.deepEqual(auditShipToolCalls(["bash", "push"]), {
|
|
243
|
-
ok: false,
|
|
244
|
-
offenders: ["push"],
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test("auditShipToolCalls: flags pr and slack", () => {
|
|
249
|
-
const r = auditShipToolCalls(["bash", "pr", "slack"]);
|
|
250
|
-
assert.equal(r.ok, false);
|
|
251
|
-
assert.deepEqual(r.offenders.sort(), ["pr", "slack"]);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("auditShipToolCalls: case-insensitive", () => {
|
|
255
|
-
assert.deepEqual(auditShipToolCalls(["Push", "SLACK"]), {
|
|
256
|
-
ok: false,
|
|
257
|
-
offenders: ["Push", "SLACK"],
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("MAKER_TOOLS: does not include any ship tool", () => {
|
|
262
|
-
const ship = new Set(["push", "pr", "slack"]);
|
|
263
|
-
for (const t of MAKER_TOOLS) {
|
|
264
|
-
assert.equal(ship.has(t), false, `maker allowlist must not include ${t}`);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
// enforceBudgetCap (FR13 — budget cap enforcement)
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
test("enforceBudgetCap: null cap never kills", () => {
|
|
273
|
-
assert.deepEqual(enforceBudgetCap({ tokens: { total: 999999 } }, null), { kill: false, reason: null });
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("enforceBudgetCap: under cap does not kill", () => {
|
|
277
|
-
assert.deepEqual(enforceBudgetCap({ tokens: { total: 100 } }, 1000), { kill: false, reason: null });
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("enforceBudgetCap: over cap kills with budget_cap_exceeded reason", () => {
|
|
281
|
-
assert.deepEqual(enforceBudgetCap({ tokens: { total: 1500 } }, 1000), { kill: true, reason: "budget_cap_exceeded" });
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("enforceBudgetCap: missing tokens treated as 0 (no kill under cap)", () => {
|
|
285
|
-
assert.deepEqual(enforceBudgetCap({}, 1000), { kill: false, reason: null });
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("enforceBudgetCap: exactly at cap does not kill (strict >)", () => {
|
|
289
|
-
assert.deepEqual(enforceBudgetCap({ tokens: { total: 1000 } }, 1000), { kill: false, reason: null });
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
// accumulateUsage (FR13 — budget cap token accumulation)
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
// Verifies the message_end listener's `+=` accumulation: each assistant
|
|
296
|
-
// message_end event contributes its per-turn Usage.totalTokens delta, and the
|
|
297
|
-
// cumulative total must equal the sum of all per-turn deltas. (User
|
|
298
|
-
// message_end events carry no .usage and must NOT contribute.)
|
|
299
|
-
|
|
300
|
-
test("accumulateUsage: sums totalTokens across two assistant message_end events", () => {
|
|
301
|
-
const events = [
|
|
302
|
-
{ type: "message_end", message: { role: "assistant", usage: { totalTokens: 1200 } } },
|
|
303
|
-
{ type: "message_end", message: { role: "assistant", usage: { totalTokens: 800 } } },
|
|
304
|
-
];
|
|
305
|
-
assert.equal(accumulateUsage(0, events), 2000);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("accumulateUsage: accumulates onto a non-zero starting total", () => {
|
|
309
|
-
const events = [
|
|
310
|
-
{ type: "message_end", message: { role: "assistant", usage: { totalTokens: 300 } } },
|
|
311
|
-
{ type: "message_end", message: { role: "assistant", usage: { totalTokens: 500 } } },
|
|
312
|
-
];
|
|
313
|
-
assert.equal(accumulateUsage(250, events), 1050);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("accumulateUsage: ignores user message_end events (no .usage)", () => {
|
|
317
|
-
const events = [
|
|
318
|
-
{ type: "message_end", message: { role: "user" } },
|
|
319
|
-
{ type: "message_end", message: { role: "assistant", usage: { totalTokens: 750 } } },
|
|
320
|
-
{ type: "message_end", message: { role: "user", usage: {} } },
|
|
321
|
-
];
|
|
322
|
-
assert.equal(accumulateUsage(0, events), 750);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
test("accumulateUsage: falls back to total / input+output when totalTokens missing", () => {
|
|
326
|
-
const events = [
|
|
327
|
-
{ type: "message_end", message: { role: "assistant", usage: { total: 410 } } },
|
|
328
|
-
{ type: "message_end", message: { role: "assistant", usage: { input: 100, output: 200 } } },
|
|
329
|
-
{ type: "message_end", message: { role: "assistant", usage: { totalTokens: 90 } } },
|
|
330
|
-
];
|
|
331
|
-
assert.equal(accumulateUsage(0, events), 800);
|
|
332
|
-
});
|