@abarcenas/ai-workflow-template 1.45.0 → 1.47.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/.agents/config/README.md +43 -0
- package/.agents/config/domain.md +37 -0
- package/.agents/instructions/learned-knowledge.instructions.md +128 -0
- package/.agents/instructions/memory-bank.instructions.md +11 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +74 -0
- package/.agents/skills/grill-me/SKILL.md +7 -0
- package/.agents/skills/grill-with-docs/SKILL.md +7 -0
- package/.agents/skills/grilling/SKILL.md +28 -0
- package/.agents/skills/prototype/LOGIC.md +67 -0
- package/.agents/skills/prototype/SKILL.md +26 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.github/workflows/npm-publish.yml +10 -0
- package/.github/workflows/pr-test.yml +31 -0
- package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
- package/.opencode/agents/scope.agent.md +299 -0
- package/.prettierignore +26 -0
- package/AGENTS.md +46 -3
- package/README.md +29 -2
- package/opencode.mcp.example.json +1 -0
- package/package.json +3 -1
- package/scripts/bump-version.test.js +238 -0
- package/scripts/mcp/playwright-mcp-launcher.js +55 -29
- package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
- package/scripts/mcp-memory-server.js +93 -76
- package/scripts/mcp-memory-server.test.js +398 -0
- package/scripts/memory-cli.test.js +297 -0
- package/scripts/memory-index.js +119 -31
- package/scripts/memory-index.test.js +1173 -0
- package/scripts/normalize-memory.js +26 -1
- package/scripts/normalize-memory.test.js +753 -0
- package/scripts/setup/discover.test.js +124 -2
- package/scripts/setup/hooks.test.js +188 -1
- package/scripts/setup/husky-init.test.js +311 -0
- package/scripts/setup/index.js +35 -5
- package/scripts/setup/index.test.js +279 -0
- package/scripts/setup/knowledgebase.test.js +100 -0
- package/scripts/setup/skills-phase.js +568 -0
- package/scripts/setup/skills-phase.test.js +1274 -0
- package/scripts/setup/sync-phase.js +6 -1
- package/scripts/setup/sync-phase.test.js +180 -2
- package/scripts/setup/ui.test.js +579 -0
- package/scripts/setup/utils.test.js +586 -0
- package/scripts/sync-unit.test.js +1881 -0
- package/scripts/sync.js +816 -239
- package/scripts/sync.test.js +283 -0
- package/scripts/vocab-sync.test.js +88 -0
- package/skills-lock.json +90 -0
|
@@ -154,7 +154,12 @@ function spawnScript(name, scriptPath, cwd, extraArgs, verbose = false) {
|
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
child.stderr.on('data', (data) => {
|
|
157
|
-
|
|
157
|
+
const chunk = data.toString()
|
|
158
|
+
output += chunk
|
|
159
|
+
// Surface warnings immediately. sync.js reports skipped locally
|
|
160
|
+
// modified files on stderr, and this phase is often the only place a
|
|
161
|
+
// consumer would ever see them (CON-04). stdout stays captured only.
|
|
162
|
+
process.stderr.write(chunk)
|
|
158
163
|
})
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -34,6 +34,7 @@ vi.mock('./utils.js', () => ({
|
|
|
34
34
|
|
|
35
35
|
import { spawn } from 'child_process'
|
|
36
36
|
import { runSyncPhase } from './sync-phase.js'
|
|
37
|
+
import { logInfo, logWarn } from './utils.js'
|
|
37
38
|
|
|
38
39
|
// ── Test helpers ──────────────────────────────────────────────────────────
|
|
39
40
|
|
|
@@ -45,15 +46,22 @@ import { runSyncPhase } from './sync-phase.js'
|
|
|
45
46
|
* This allows the Promise-based flow in spawnScript() to resolve naturally.
|
|
46
47
|
*
|
|
47
48
|
* @param {object} opts
|
|
48
|
-
* @param {number}
|
|
49
|
+
* @param {number|null} [opts.exitCode=0] - Exit code for the 'close' event
|
|
49
50
|
* @param {string} [opts.stdoutData=''] - Data emitted via stdout 'data'
|
|
50
51
|
* @param {string} [opts.stderrData=''] - Data emitted via stderr 'data'
|
|
52
|
+
* @param {boolean} [opts.emitError=false] - Emit an 'error' event instead
|
|
51
53
|
* @returns {object} Mock child process with .stdout.on, .stderr.on, .on
|
|
52
54
|
*/
|
|
53
|
-
function createMockChild({
|
|
55
|
+
function createMockChild({
|
|
56
|
+
exitCode = 0,
|
|
57
|
+
stdoutData = '',
|
|
58
|
+
stderrData = '',
|
|
59
|
+
emitError = false,
|
|
60
|
+
} = {}) {
|
|
54
61
|
const stdoutHandlers = []
|
|
55
62
|
const stderrHandlers = []
|
|
56
63
|
let closeHandler = null
|
|
64
|
+
let errorHandler = null
|
|
57
65
|
|
|
58
66
|
const child = {
|
|
59
67
|
stdout: {
|
|
@@ -68,11 +76,18 @@ function createMockChild({ exitCode = 0, stdoutData = '', stderrData = '' } = {}
|
|
|
68
76
|
},
|
|
69
77
|
on: (event, handler) => {
|
|
70
78
|
if (event === 'close') closeHandler = handler
|
|
79
|
+
if (event === 'error') errorHandler = handler
|
|
71
80
|
},
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
// Fire events on a microtask so handlers are registered first
|
|
75
84
|
Promise.resolve().then(() => {
|
|
85
|
+
if (emitError) {
|
|
86
|
+
if (errorHandler) {
|
|
87
|
+
errorHandler(new Error('spawn ENOENT'))
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
76
91
|
if (stdoutData) {
|
|
77
92
|
stdoutHandlers.forEach((h) => h(Buffer.from(stdoutData)))
|
|
78
93
|
}
|
|
@@ -193,3 +208,166 @@ describe('sync-phase verbose spawn behavior', () => {
|
|
|
193
208
|
}
|
|
194
209
|
})
|
|
195
210
|
})
|
|
211
|
+
|
|
212
|
+
// ── Execution paths (skip / dry-run / failures / forwarding) ───────────────
|
|
213
|
+
|
|
214
|
+
describe('sync-phase execution paths', () => {
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
vi.clearAllMocks()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('skipSync=true returns "skipped" without spawning anything', async () => {
|
|
220
|
+
const result = await runSyncPhase(makeContext({ skipSync: true }))
|
|
221
|
+
|
|
222
|
+
expect(result).toEqual({
|
|
223
|
+
action: 'skipped',
|
|
224
|
+
message: 'Sync phase skipped (--skip-sync)',
|
|
225
|
+
results: [],
|
|
226
|
+
})
|
|
227
|
+
expect(spawn).not.toHaveBeenCalled()
|
|
228
|
+
expect(logInfo).toHaveBeenCalledWith(
|
|
229
|
+
'[setup] Sync phase skipped (--skip-sync)',
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('dryRun=true returns "dry-run" and mentions the consumer root', async () => {
|
|
234
|
+
const result = await runSyncPhase(makeContext({ dryRun: true }))
|
|
235
|
+
|
|
236
|
+
expect(result.action).toBe('dry-run')
|
|
237
|
+
expect(result.message).toContain('sync.js')
|
|
238
|
+
expect(result.message).toContain('normalize-memory.js')
|
|
239
|
+
expect(result.message).toContain('/fake/consumer')
|
|
240
|
+
expect(result.results).toEqual([])
|
|
241
|
+
expect(spawn).not.toHaveBeenCalled()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('forwards --force to both child scripts when flags.force is set', async () => {
|
|
245
|
+
vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
|
|
246
|
+
|
|
247
|
+
await runSyncPhase(makeContext({ flags: { force: true } }))
|
|
248
|
+
|
|
249
|
+
const argv = vi.mocked(spawn).mock.calls.map((c) => c[1])
|
|
250
|
+
expect(argv).toHaveLength(2)
|
|
251
|
+
for (const args of argv) {
|
|
252
|
+
expect(args).toContain('--force')
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('does not forward --force by default', async () => {
|
|
257
|
+
vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
|
|
258
|
+
|
|
259
|
+
await runSyncPhase(makeContext({ flags: {} }))
|
|
260
|
+
|
|
261
|
+
for (const call of vi.mocked(spawn).mock.calls) {
|
|
262
|
+
expect(call[1]).not.toContain('--force')
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('treats a non-zero exit as partial and logs each failing script', async () => {
|
|
267
|
+
vi.mocked(spawn)
|
|
268
|
+
.mockImplementationOnce(() => createMockChild({ exitCode: 1 }))
|
|
269
|
+
.mockImplementationOnce(() => createMockChild({ exitCode: 0 }))
|
|
270
|
+
|
|
271
|
+
const result = await runSyncPhase(makeContext())
|
|
272
|
+
|
|
273
|
+
expect(result.action).toBe('partial')
|
|
274
|
+
expect(result.message).toBe(
|
|
275
|
+
'Sync phase completed with 1 script failure(s)',
|
|
276
|
+
)
|
|
277
|
+
expect(result.results).toHaveLength(2)
|
|
278
|
+
expect(result.results[0]).toMatchObject({ script: 'sync.js', success: false, exitCode: 1 })
|
|
279
|
+
expect(result.results[1]).toMatchObject({ script: 'normalize-memory.js', success: true })
|
|
280
|
+
|
|
281
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
282
|
+
'[setup] Sync phase completed with 1 script failure(s)',
|
|
283
|
+
)
|
|
284
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
285
|
+
'[setup] sync.js exited with code 1',
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('counts every failing script in the partial message', async () => {
|
|
290
|
+
vi.mocked(spawn)
|
|
291
|
+
.mockImplementationOnce(() => createMockChild({ exitCode: 2 }))
|
|
292
|
+
.mockImplementationOnce(() => createMockChild({ exitCode: 3 }))
|
|
293
|
+
|
|
294
|
+
const result = await runSyncPhase(makeContext())
|
|
295
|
+
|
|
296
|
+
expect(result.action).toBe('partial')
|
|
297
|
+
expect(result.message).toBe('Sync phase completed with 2 script failure(s)')
|
|
298
|
+
expect(logWarn).toHaveBeenCalledWith('[setup] sync.js exited with code 2')
|
|
299
|
+
expect(logWarn).toHaveBeenCalledWith(
|
|
300
|
+
'[setup] normalize-memory.js exited with code 3',
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('forwards child stderr to the parent stderr in non-verbose mode', async () => {
|
|
305
|
+
const writeSpy = vi
|
|
306
|
+
.spyOn(process.stderr, 'write')
|
|
307
|
+
.mockImplementation(() => true)
|
|
308
|
+
vi.mocked(spawn).mockImplementation(() =>
|
|
309
|
+
createMockChild({
|
|
310
|
+
exitCode: 0,
|
|
311
|
+
stdoutData: 'copied files\n',
|
|
312
|
+
stderrData: 'skipped locally modified file\n',
|
|
313
|
+
}),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const result = await runSyncPhase(makeContext({ verbose: false }))
|
|
318
|
+
|
|
319
|
+
// Warning text reaches the terminal even though stdout is captured
|
|
320
|
+
expect(writeSpy).toHaveBeenCalledWith('skipped locally modified file\n')
|
|
321
|
+
// ...and the same text is preserved in the captured output
|
|
322
|
+
for (const r of result.results) {
|
|
323
|
+
expect(r.output).toContain('copied files')
|
|
324
|
+
expect(r.output).toContain('skipped locally modified file')
|
|
325
|
+
}
|
|
326
|
+
} finally {
|
|
327
|
+
writeSpy.mockRestore()
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('verbose mode streams output and does not capture it', async () => {
|
|
332
|
+
vi.mocked(spawn).mockImplementation(() => createMockChild({ exitCode: 0 }))
|
|
333
|
+
|
|
334
|
+
const result = await runSyncPhase(makeContext({ verbose: true }))
|
|
335
|
+
|
|
336
|
+
expect(result.action).toBe('completed')
|
|
337
|
+
for (const r of result.results) {
|
|
338
|
+
expect(r.output).toBe('(streamed to terminal)')
|
|
339
|
+
}
|
|
340
|
+
for (const call of vi.mocked(spawn).mock.calls) {
|
|
341
|
+
expect(call[2].stdio).toBe('inherit')
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('resolves a spawn error event into a failed result instead of throwing', async () => {
|
|
346
|
+
vi.mocked(spawn).mockImplementation(() => createMockChild({ emitError: true }))
|
|
347
|
+
|
|
348
|
+
const result = await runSyncPhase(makeContext())
|
|
349
|
+
|
|
350
|
+
expect(result.action).toBe('partial')
|
|
351
|
+
expect(result.results).toHaveLength(2)
|
|
352
|
+
for (const r of result.results) {
|
|
353
|
+
expect(r.success).toBe(false)
|
|
354
|
+
expect(r.exitCode).toBe(-1)
|
|
355
|
+
expect(r.output).toContain('Failed to spawn child process')
|
|
356
|
+
expect(r.output).toContain('spawn ENOENT')
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('normalises a null exit code (killed by signal) to -1', async () => {
|
|
361
|
+
vi.mocked(spawn).mockImplementation(() =>
|
|
362
|
+
createMockChild({ exitCode: null }),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
const result = await runSyncPhase(makeContext())
|
|
366
|
+
|
|
367
|
+
expect(result.action).toBe('partial')
|
|
368
|
+
for (const r of result.results) {
|
|
369
|
+
expect(r.success).toBe(false)
|
|
370
|
+
expect(r.exitCode).toBe(-1)
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
})
|