@abarcenas/ai-workflow-template 1.46.0 → 1.48.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 +2 -2
- package/.agents/instructions/agent.instructions.md +16 -16
- package/.agents/instructions/context7.instructions.md +40 -0
- package/.agents/instructions/learned-knowledge.instructions.md +18 -0
- package/.agents/instructions/memory-bank.instructions.md +3 -3
- package/.opencode/agents/{architect.agent.md → architect.md} +5 -6
- package/.opencode/agents/{coder.agent.md → coder.md} +4 -8
- package/.opencode/agents/{deployer.agent.md → deployer.md} +4 -9
- package/.opencode/agents/{designer.agent.md → designer.md} +5 -6
- package/.opencode/agents/{e2e-tester.agent.md → e2e-tester.md} +4 -7
- package/.opencode/agents/{implementer.agent.md → implementer.md} +4 -8
- package/.opencode/agents/orchestrator/{feature-pipeline.agent.md → feature-pipeline.md} +16 -19
- package/.opencode/agents/orchestrator/{orchestrator.agent.md → orchestrator.md} +26 -34
- package/.opencode/agents/orchestrator/{tdd-orchestrator.agent.md → tdd-orchestrator.md} +26 -29
- package/.opencode/agents/{plan.agent.md → planner.md} +4 -9
- package/.opencode/agents/{researcher.agent.md → researcher.md} +5 -9
- package/.opencode/agents/{reviewer.agent.md → reviewer.md} +5 -8
- package/.opencode/agents/{scope.agent.md → scope.md} +5 -9
- package/.opencode/agents/{tracker.agent.md → tracker.md} +5 -9
- package/.opencode/agents/{unit-tester.agent.md → unit-tester.md} +4 -8
- package/.prettierignore +26 -0
- package/AGENTS.md +10 -6
- package/README.md +36 -12
- package/opencode.mcp.example.json +13 -12
- package/package.json +3 -1
- package/scripts/sync-unit.test.js +551 -12
- package/scripts/sync.js +296 -13
package/scripts/sync.js
CHANGED
|
@@ -19,6 +19,27 @@ export const excludedRelativePaths = new Set([
|
|
|
19
19
|
'instructions/learned-knowledge.instructions.md',
|
|
20
20
|
])
|
|
21
21
|
|
|
22
|
+
// Files the consumer owns outright. Sync never copies, overwrites, or prunes
|
|
23
|
+
// them — not even with --force — because local edits are the point, not drift.
|
|
24
|
+
// Matching is channel-agnostic: every copy pass compares its own relative path
|
|
25
|
+
// against this set, so a root entry such as "opencode.json" also protects
|
|
26
|
+
// ".opencode/opencode.json" and ".agents/opencode.json". That over-match is
|
|
27
|
+
// intentional and conservative. The original learned-knowledge exclusion is
|
|
28
|
+
// spread in so there is one rule, not two parallel checks.
|
|
29
|
+
export const projectOwnedPaths = new Set([
|
|
30
|
+
'AGENTS.project.md',
|
|
31
|
+
'opencode.json',
|
|
32
|
+
'.prettierignore',
|
|
33
|
+
// Reserved: sync protects this file from overwrite and prune, but it does
|
|
34
|
+
// NOT read or honour its globs yet. Do not present it as functional.
|
|
35
|
+
'.agents-sync-ignore',
|
|
36
|
+
...excludedRelativePaths,
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
export function isProjectOwnedPath(relativePath) {
|
|
40
|
+
return projectOwnedPaths.has(relativePath)
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
export const openCodeExcluded = new Set([
|
|
23
44
|
'node_modules',
|
|
24
45
|
'package.json',
|
|
@@ -27,7 +48,10 @@ export const openCodeExcluded = new Set([
|
|
|
27
48
|
'.gitignore',
|
|
28
49
|
])
|
|
29
50
|
|
|
30
|
-
|
|
51
|
+
// Never add a project-owned file here (AGENTS.project.md, opencode.json,
|
|
52
|
+
// .prettierignore): those are scaffolded create-if-absent and must not ship
|
|
53
|
+
// from the package. opencode.json is governed solely by projectOwnedPaths.
|
|
54
|
+
export const rootFiles = ['AGENTS.md', 'skills-lock.json']
|
|
31
55
|
|
|
32
56
|
export const scriptsToSync = [
|
|
33
57
|
'memory-cli.js',
|
|
@@ -44,6 +68,153 @@ export const scriptsToSync = [
|
|
|
44
68
|
'knowledgebase-init.sql',
|
|
45
69
|
]
|
|
46
70
|
|
|
71
|
+
// The hash-managed paths a consumer's .prettierignore must cover so a format
|
|
72
|
+
// pass cannot freeze a sync. Directories carry a trailing slash so a pattern
|
|
73
|
+
// can cover their subtree. AGENTS.project.md is deliberately absent: it is
|
|
74
|
+
// project-owned, so formatting it cannot freeze a sync.
|
|
75
|
+
export const requiredPrettierIgnorePatterns = [
|
|
76
|
+
'.agents/',
|
|
77
|
+
'.opencode/agents/',
|
|
78
|
+
'.opencode/commands/',
|
|
79
|
+
...rootFiles,
|
|
80
|
+
...scriptsToSync.map((script) => `scripts/${script}`),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const prettierIgnoreWarning =
|
|
84
|
+
'[ai-workflow-template] Existing .prettierignore does not protect managed files; merge the patterns from node_modules/@abarcenas/ai-workflow-template/.prettierignore'
|
|
85
|
+
|
|
86
|
+
const ignoreAllPatterns = new Set(['', '.', '*', '**', '/'])
|
|
87
|
+
|
|
88
|
+
function normalizeIgnorePattern(pattern) {
|
|
89
|
+
const trimmed = pattern.trim()
|
|
90
|
+
return trimmed === '/'
|
|
91
|
+
? '/'
|
|
92
|
+
: trimmed.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Gitignore lines, preserving negation and treating an inline "# ..." literally.
|
|
96
|
+
// Escaped leading "#" or "!" are literals, not comments/negations.
|
|
97
|
+
export function parsePrettierIgnoreRules(contents) {
|
|
98
|
+
const rules = []
|
|
99
|
+
|
|
100
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
101
|
+
const line = rawLine.trim()
|
|
102
|
+
if (!line || line.startsWith('#')) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (line.startsWith('\\#') || line.startsWith('\\!')) {
|
|
107
|
+
rules.push({
|
|
108
|
+
negated: false,
|
|
109
|
+
pattern: normalizeIgnorePattern(line.slice(1)),
|
|
110
|
+
})
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const negated = line.startsWith('!')
|
|
115
|
+
const pattern = normalizeIgnorePattern(negated ? line.slice(1) : line)
|
|
116
|
+
if (pattern) {
|
|
117
|
+
rules.push({ negated, pattern })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return rules
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// True when a pattern ignores targetPath or one of its ancestors. A "base/**"
|
|
125
|
+
// pattern ignores strict descendants only, so it never covers the file "base".
|
|
126
|
+
function patternAppliesTo(pattern, targetPath) {
|
|
127
|
+
if (ignoreAllPatterns.has(pattern)) {
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
if (pattern.endsWith('/**')) {
|
|
131
|
+
return targetPath.startsWith(`${pattern.slice(0, -3)}/`)
|
|
132
|
+
}
|
|
133
|
+
if (pattern.startsWith('**/')) {
|
|
134
|
+
const suffix = pattern.slice(3)
|
|
135
|
+
return targetPath === suffix || targetPath.endsWith(`/${suffix}`)
|
|
136
|
+
}
|
|
137
|
+
if (/[*?[]/.test(pattern)) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return targetPath === pattern || targetPath.startsWith(`${pattern}/`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A negation below a managed directory punches a hole in that directory's
|
|
144
|
+
// subtree, so it removes coverage until a later positive restores the subtree.
|
|
145
|
+
function ignorePatternBase(pattern) {
|
|
146
|
+
return normalizeIgnorePattern(
|
|
147
|
+
pattern.endsWith('/**') ? pattern.slice(0, -3) : pattern,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function patternAppliesBelow(pattern, directoryPath) {
|
|
152
|
+
if (ignoreAllPatterns.has(pattern)) {
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const base = ignorePatternBase(pattern)
|
|
157
|
+
return (
|
|
158
|
+
!/[*?[]/.test(base) &&
|
|
159
|
+
base.startsWith(directoryPath) &&
|
|
160
|
+
base !== normalizeIgnorePattern(directoryPath)
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Last-match-wins over the rule list, matching gitignore semantics. Required
|
|
165
|
+
// directories also track negated subtrees so a later positive can restore them.
|
|
166
|
+
function isCovered(requiredPath, rules) {
|
|
167
|
+
const isDirectory = requiredPath.endsWith('/')
|
|
168
|
+
let covered = false
|
|
169
|
+
const holes = new Set()
|
|
170
|
+
|
|
171
|
+
for (const { negated, pattern } of rules) {
|
|
172
|
+
if (!isDirectory) {
|
|
173
|
+
if (patternAppliesTo(pattern, requiredPath)) {
|
|
174
|
+
covered = !negated
|
|
175
|
+
}
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (negated) {
|
|
180
|
+
if (patternAppliesTo(pattern, requiredPath)) {
|
|
181
|
+
covered = false
|
|
182
|
+
holes.clear()
|
|
183
|
+
} else if (patternAppliesBelow(pattern, requiredPath)) {
|
|
184
|
+
holes.add(ignorePatternBase(pattern))
|
|
185
|
+
}
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (patternAppliesTo(pattern, requiredPath)) {
|
|
190
|
+
covered = true
|
|
191
|
+
holes.clear()
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (patternAppliesBelow(pattern, requiredPath)) {
|
|
196
|
+
const base = ignorePatternBase(pattern)
|
|
197
|
+
for (const hole of holes) {
|
|
198
|
+
if (hole === base || hole.startsWith(`${base}/`)) {
|
|
199
|
+
holes.delete(hole)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return covered && holes.size === 0
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function missingPrettierIgnorePatterns(
|
|
209
|
+
contents,
|
|
210
|
+
requiredPatterns = requiredPrettierIgnorePatterns,
|
|
211
|
+
) {
|
|
212
|
+
const rules = parsePrettierIgnoreRules(contents)
|
|
213
|
+
return requiredPatterns.filter(
|
|
214
|
+
(requiredPath) => !isCovered(requiredPath, rules),
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
47
218
|
export const memoryBankStubs = {
|
|
48
219
|
'projectbrief.md': `# projectbrief
|
|
49
220
|
|
|
@@ -106,6 +277,49 @@ Cross-session knowledge accumulated by agent pipelines. Each session records dis
|
|
|
106
277
|
`,
|
|
107
278
|
}
|
|
108
279
|
|
|
280
|
+
// Project-owned files are scaffolded create-if-absent and then left alone for
|
|
281
|
+
// the lifetime of the consumer project (deleting one recreates it on the next
|
|
282
|
+
// sync). The .prettierignore text here must stay byte-identical to the
|
|
283
|
+
// repository's shipped .prettierignore (asserted in sync-unit.test.js) so the
|
|
284
|
+
// tarball copy and the scaffold cannot drift.
|
|
285
|
+
export const projectOwnedStubs = {
|
|
286
|
+
'AGENTS.project.md': `# Project Rules
|
|
287
|
+
|
|
288
|
+
> Project-owned. This file is never overwritten or pruned by sync — edit it freely.
|
|
289
|
+
|
|
290
|
+
## Project-specific agent rules
|
|
291
|
+
|
|
292
|
+
- {{add project-specific rules here}}
|
|
293
|
+
`,
|
|
294
|
+
'.prettierignore': `# Project-owned. Sync never overwrites this file.
|
|
295
|
+
# Keep formatters away from files managed by @abarcenas/ai-workflow-template:
|
|
296
|
+
# a formatting pass over managed files makes every future sync skip them.
|
|
297
|
+
|
|
298
|
+
# Managed agent configuration
|
|
299
|
+
.agents/
|
|
300
|
+
.opencode/agents/
|
|
301
|
+
.opencode/commands/
|
|
302
|
+
|
|
303
|
+
# Managed root files
|
|
304
|
+
AGENTS.md
|
|
305
|
+
AGENTS.project.md
|
|
306
|
+
skills-lock.json
|
|
307
|
+
|
|
308
|
+
# Managed scripts (scripts/sync.js scriptsToSync)
|
|
309
|
+
scripts/memory-cli.js
|
|
310
|
+
scripts/memory-index.js
|
|
311
|
+
scripts/bump-version.js
|
|
312
|
+
scripts/validate-memory-schema.js
|
|
313
|
+
scripts/vocab-sync.js
|
|
314
|
+
scripts/mcp-memory-server.js
|
|
315
|
+
scripts/mcp/playwright-mcp-launcher.js
|
|
316
|
+
scripts/knowledgebase-cli.js
|
|
317
|
+
scripts/knowledgebase-index.js
|
|
318
|
+
scripts/mcp-knowledgebase-server.js
|
|
319
|
+
scripts/knowledgebase-init.sql
|
|
320
|
+
`,
|
|
321
|
+
}
|
|
322
|
+
|
|
109
323
|
// Parses the flags supported by the sync entry point. Unknown flags are ignored
|
|
110
324
|
// so that consumer tooling can pass extra arguments without breaking a sync.
|
|
111
325
|
export function parseArgs(argv = [], env = process.env) {
|
|
@@ -263,10 +477,16 @@ export function isPathInside(baseDir, candidatePath) {
|
|
|
263
477
|
// Resolves a manifest key to its package path (for the "still shipped?" check)
|
|
264
478
|
// and its consumer path (the prune candidate). Returns null for keys that the
|
|
265
479
|
// sync layer does not own, and for paths that escape their channel root.
|
|
266
|
-
export function resolvePruneEntry(manifestKey, { pruneChannels
|
|
480
|
+
export function resolvePruneEntry(manifestKey, { pruneChannels }) {
|
|
267
481
|
let channel
|
|
268
482
|
let relativePath
|
|
269
483
|
|
|
484
|
+
// Scaffold-only namespace for project-owned files. There is no package source
|
|
485
|
+
// to compare against and nothing here may ever be deleted.
|
|
486
|
+
if (manifestKey.startsWith('__project__/')) {
|
|
487
|
+
return null
|
|
488
|
+
}
|
|
489
|
+
|
|
270
490
|
if (manifestKey.startsWith('__opencode__/')) {
|
|
271
491
|
channel = pruneChannels.opencode
|
|
272
492
|
relativePath = manifestKey.slice(channel.prefix.length)
|
|
@@ -289,22 +509,20 @@ export function resolvePruneEntry(manifestKey, { pruneChannels, packageRoot }) {
|
|
|
289
509
|
} else {
|
|
290
510
|
channel = pruneChannels.agents
|
|
291
511
|
relativePath = manifestKey
|
|
292
|
-
if (excludedRelativePaths.has(relativePath)) {
|
|
293
|
-
return null
|
|
294
|
-
}
|
|
295
512
|
}
|
|
296
513
|
|
|
297
514
|
if (!relativePath) {
|
|
298
515
|
return null
|
|
299
516
|
}
|
|
300
517
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
packagePaths.push(resolve(packageRoot, 'opencode.mcp.example.json'))
|
|
518
|
+
// Project-owned files survive every prune, even when the package no longer
|
|
519
|
+
// ships a same-named path. This is the single check for all four channels.
|
|
520
|
+
if (isProjectOwnedPath(relativePath)) {
|
|
521
|
+
return null
|
|
306
522
|
}
|
|
307
523
|
|
|
524
|
+
const packagePaths = [resolve(channel.packageDir, relativePath)]
|
|
525
|
+
|
|
308
526
|
const consumerPath = resolve(channel.consumerDir, relativePath)
|
|
309
527
|
if (!isPathInside(channel.packageDir, packagePaths[0])) {
|
|
310
528
|
return null
|
|
@@ -356,9 +574,11 @@ export function syncProject(context) {
|
|
|
356
574
|
|
|
357
575
|
/* ── Sync .agents/ directory ────────────────────────────────── */
|
|
358
576
|
|
|
577
|
+
// Project ownership outranks every flag: these paths are skipped before the
|
|
578
|
+
// --force branch below, so --force can never replace consumer-owned content.
|
|
359
579
|
for (const sourceFile of sourceFiles) {
|
|
360
580
|
const relativePath = relative(sourceAgentsDir, sourceFile)
|
|
361
|
-
if (
|
|
581
|
+
if (isProjectOwnedPath(relativePath)) {
|
|
362
582
|
continue
|
|
363
583
|
}
|
|
364
584
|
|
|
@@ -396,7 +616,11 @@ export function syncProject(context) {
|
|
|
396
616
|
|
|
397
617
|
for (const sourceFile of openCodeFiles) {
|
|
398
618
|
const relativePath = relative(sourceOpenCodeDir, sourceFile)
|
|
399
|
-
if (
|
|
619
|
+
if (
|
|
620
|
+
isProjectOwnedPath(relativePath) ||
|
|
621
|
+
openCodeExcluded.has(relativePath) ||
|
|
622
|
+
openCodeExcluded.has(relativePath.split('/')[0])
|
|
623
|
+
) {
|
|
400
624
|
continue
|
|
401
625
|
}
|
|
402
626
|
|
|
@@ -434,6 +658,10 @@ export function syncProject(context) {
|
|
|
434
658
|
let scriptsSkipped = 0
|
|
435
659
|
|
|
436
660
|
for (const scriptRelPath of scriptsToSync) {
|
|
661
|
+
if (isProjectOwnedPath(scriptRelPath)) {
|
|
662
|
+
continue
|
|
663
|
+
}
|
|
664
|
+
|
|
437
665
|
const sourceFile = resolve(sourceScriptsDir, scriptRelPath)
|
|
438
666
|
const targetFile = resolve(targetScriptsDir, scriptRelPath)
|
|
439
667
|
|
|
@@ -473,6 +701,12 @@ export function syncProject(context) {
|
|
|
473
701
|
/* ── Sync root files ────────────────────────────────────────── */
|
|
474
702
|
|
|
475
703
|
for (const rootFile of rootFiles) {
|
|
704
|
+
// Defense in depth: projectOwnedPaths wins if a project-owned name is ever
|
|
705
|
+
// added to rootFiles. opencode.json is no longer in this allowlist at all.
|
|
706
|
+
if (isProjectOwnedPath(rootFile)) {
|
|
707
|
+
continue
|
|
708
|
+
}
|
|
709
|
+
|
|
476
710
|
const sourceFile = resolve(packageRoot, rootFile)
|
|
477
711
|
if (!existsSync(sourceFile)) {
|
|
478
712
|
continue
|
|
@@ -542,11 +776,60 @@ export function syncProject(context) {
|
|
|
542
776
|
scaffolded += 1
|
|
543
777
|
}
|
|
544
778
|
|
|
779
|
+
/* ── Scaffold project-owned files ───────────────────────────── */
|
|
780
|
+
|
|
781
|
+
// Create-if-absent only. The __project__ namespace is reporting metadata
|
|
782
|
+
// written when a file is first created by sync; pre-existing consumer files
|
|
783
|
+
// are intentionally absent, because there is no managed baseline to report.
|
|
784
|
+
// It is never resolved by the copy or prune passes and never drives an
|
|
785
|
+
// overwrite. Not refreshing it on later runs keeps a consumer-owned file from
|
|
786
|
+
// producing manifest churn on every install.
|
|
787
|
+
const targetPrettierIgnore = resolve(consumerRoot, '.prettierignore')
|
|
788
|
+
const prettierIgnoreExisted = existsSync(targetPrettierIgnore)
|
|
789
|
+
|
|
790
|
+
for (const [fileName, fileContent] of Object.entries(projectOwnedStubs)) {
|
|
791
|
+
const targetFile = resolve(consumerRoot, fileName)
|
|
792
|
+
const trackedKey = `__project__/${fileName}`
|
|
793
|
+
let didScaffold = false
|
|
794
|
+
|
|
795
|
+
if (!existsSync(targetFile)) {
|
|
796
|
+
ensureParentDirectory(targetFile)
|
|
797
|
+
if (isVerbose) console.error(` … scaffolding: ${fileName}`)
|
|
798
|
+
writeFileSync(targetFile, fileContent, 'utf8')
|
|
799
|
+
scaffolded += 1
|
|
800
|
+
didScaffold = true
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Only hash what this run created, and only if it is a regular file: a
|
|
804
|
+
// directory or unreadable path at this location must not abort the run.
|
|
805
|
+
if (didScaffold && isRegularFile(targetFile)) {
|
|
806
|
+
manifest.files[trackedKey] = hashFile(targetFile)
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// An existing .prettierignore is project-owned and is never edited. Tell the
|
|
811
|
+
// consumer when it leaves managed files exposed, once and deterministically.
|
|
812
|
+
if (prettierIgnoreExisted) {
|
|
813
|
+
try {
|
|
814
|
+
const missingPatterns = missingPrettierIgnorePatterns(
|
|
815
|
+
readFileSync(targetPrettierIgnore, 'utf8'),
|
|
816
|
+
)
|
|
817
|
+
if (missingPatterns.length > 0) {
|
|
818
|
+
console.warn(prettierIgnoreWarning)
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
// A directory or unreadable path at .prettierignore is project-owned
|
|
822
|
+
// state we cannot inspect. Skip the advisory rather than abort the run.
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
545
826
|
/* ── Auto-copy opencode.mcp.example.json → opencode.json ────── */
|
|
546
827
|
|
|
828
|
+
// Existing consumer configs are never rewritten. The example carries the
|
|
829
|
+
// `instructions` entry that loads AGENTS.project.md for fresh consumers.
|
|
547
830
|
const exampleMcpFile = resolve(packageRoot, 'opencode.mcp.example.json')
|
|
548
831
|
const targetMcpFile = resolve(consumerRoot, 'opencode.json')
|
|
549
|
-
const mcpTrackedKey = '
|
|
832
|
+
const mcpTrackedKey = '__project__/opencode.json'
|
|
550
833
|
|
|
551
834
|
if (existsSync(exampleMcpFile) && !existsSync(targetMcpFile)) {
|
|
552
835
|
if (isVerbose) console.error(' … scaffolding: opencode.json from example')
|