@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.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.
Files changed (124) hide show
  1. package/.mcp.json +8 -0
  2. package/CHANGELOG.md +78 -0
  3. package/CONTRIBUTING.md +54 -0
  4. package/LICENSE +21 -0
  5. package/README.md +304 -0
  6. package/SECURITY.md +50 -0
  7. package/agents/sw-admin-frontend-developer.md +52 -0
  8. package/agents/sw-php-backend-developer.md +63 -0
  9. package/agents/sw-product-manager.md +62 -0
  10. package/agents/sw-qa-engineer.md +160 -0
  11. package/agents/sw-shopware-architect.md +79 -0
  12. package/agents/sw-storefront-developer.md +93 -0
  13. package/agents/sw-tender-editor.md +43 -0
  14. package/bin/cli.mjs +73 -0
  15. package/codex/agents/sw-admin-frontend-developer.toml +54 -0
  16. package/codex/agents/sw-php-backend-developer.toml +65 -0
  17. package/codex/agents/sw-product-manager.toml +66 -0
  18. package/codex/agents/sw-qa-engineer.toml +161 -0
  19. package/codex/agents/sw-shopware-architect.toml +81 -0
  20. package/codex/agents/sw-storefront-developer.toml +96 -0
  21. package/codex/agents/sw-tender-editor.toml +50 -0
  22. package/content/AGENTS.md +63 -0
  23. package/content/claude-settings.json +31 -0
  24. package/content/mcp-servers.json +18 -0
  25. package/copilot/agents/sw-admin-frontend-developer.agent.md +53 -0
  26. package/copilot/agents/sw-php-backend-developer.agent.md +64 -0
  27. package/copilot/agents/sw-product-manager.agent.md +65 -0
  28. package/copilot/agents/sw-qa-engineer.agent.md +160 -0
  29. package/copilot/agents/sw-shopware-architect.agent.md +80 -0
  30. package/copilot/agents/sw-storefront-developer.agent.md +94 -0
  31. package/copilot/agents/sw-tender-editor.agent.md +46 -0
  32. package/lib/actions.mjs +449 -0
  33. package/lib/commands.mjs +446 -0
  34. package/lib/companions.mjs +117 -0
  35. package/lib/content.mjs +101 -0
  36. package/lib/fsx.mjs +188 -0
  37. package/lib/guide.mjs +98 -0
  38. package/lib/hosts.mjs +283 -0
  39. package/lib/lock.mjs +158 -0
  40. package/lib/merge.mjs +338 -0
  41. package/lib/out.mjs +74 -0
  42. package/mcp.json +10 -0
  43. package/package.json +58 -0
  44. package/plugin.json +19 -0
  45. package/skills/sw-design-requirements/SKILL.md +102 -0
  46. package/skills/sw-design-requirements/reference/clarification-loop.md +20 -0
  47. package/skills/sw-design-requirements/reference/confidence-rubric.md +82 -0
  48. package/skills/sw-design-requirements/reference/consistency-probes.md +65 -0
  49. package/skills/sw-design-requirements/reference/editor-gate.md +15 -0
  50. package/skills/sw-design-requirements/reference/editor-mode.md +15 -0
  51. package/skills/sw-design-requirements/reference/prd-confidence-checklist.md +95 -0
  52. package/skills/sw-design-requirements/reference/prd-template.md +153 -0
  53. package/skills/sw-design-requirements/reference/readiness-and-report.md +21 -0
  54. package/skills/sw-design-requirements/reference/splitting-guidelines.md +69 -0
  55. package/skills/sw-design-solution/SKILL.md +48 -0
  56. package/skills/sw-design-solution/reference/adr-flow.md +13 -0
  57. package/skills/sw-design-solution/reference/adr-template.md +40 -0
  58. package/skills/sw-design-solution/reference/clarification-loop.md +13 -0
  59. package/skills/sw-design-solution/reference/draft-and-spawn.md +21 -0
  60. package/skills/sw-design-solution/reference/editor-mode.md +16 -0
  61. package/skills/sw-design-solution/reference/pre-check-and-target.md +19 -0
  62. package/skills/sw-design-solution/reference/readiness-and-report.md +15 -0
  63. package/skills/sw-design-solution/reference/research-and-evidence.md +25 -0
  64. package/skills/sw-design-solution/reference/tech-spec-template.md +112 -0
  65. package/skills/sw-design-solution/reference/toolchain-conformance.md +9 -0
  66. package/skills/sw-discover-tender/SKILL.md +68 -0
  67. package/skills/sw-discover-tender/reference/agent-briefs.md +135 -0
  68. package/skills/sw-discover-tender/reference/analysis-template.md +224 -0
  69. package/skills/sw-discover-tender/reference/assumption-catalogue.md +104 -0
  70. package/skills/sw-discover-tender/reference/client-question-rules.md +47 -0
  71. package/skills/sw-discover-tender/reference/confidence-rubric.md +80 -0
  72. package/skills/sw-discover-tender/reference/context-parameters.md +116 -0
  73. package/skills/sw-discover-tender/reference/coverage-mapping.md +62 -0
  74. package/skills/sw-discover-tender/reference/editor-mode.md +11 -0
  75. package/skills/sw-discover-tender/reference/editor-session.md +115 -0
  76. package/skills/sw-discover-tender/reference/estimation-model.md +106 -0
  77. package/skills/sw-discover-tender/reference/procedure.md +98 -0
  78. package/skills/sw-discover-tender/reference/response-rules.md +13 -0
  79. package/skills/sw-document-feature/SKILL.md +64 -0
  80. package/skills/sw-document-feature/reference/adr-promotion.md +64 -0
  81. package/skills/sw-document-feature/reference/built-check.md +71 -0
  82. package/skills/sw-document-feature/reference/domain-guidelines.md +59 -0
  83. package/skills/sw-document-feature/reference/page-rules.md +129 -0
  84. package/skills/sw-document-feature/reference/procedure.md +58 -0
  85. package/skills/sw-document-feature/reference/scaffold/README.md +78 -0
  86. package/skills/sw-document-feature/reference/scaffold/_config.yml +51 -0
  87. package/skills/sw-document-feature/reference/scaffold/adr/index.md +44 -0
  88. package/skills/sw-document-feature/reference/scaffold/baseline/glossary.md +55 -0
  89. package/skills/sw-document-feature/reference/scaffold/baseline/tech-stack.md +48 -0
  90. package/skills/sw-document-feature/reference/scaffold/domains/index.md +55 -0
  91. package/skills/sw-document-feature/reference/scaffold/domains/platform/configuration.md +54 -0
  92. package/skills/sw-document-feature/reference/scaffold/domains/platform/customization-guidelines.md +47 -0
  93. package/skills/sw-document-feature/reference/scaffold/domains/platform/debugging.md +55 -0
  94. package/skills/sw-document-feature/reference/scaffold/domains/platform/environments-and-deployment.md +49 -0
  95. package/skills/sw-document-feature/reference/scaffold/domains/platform/extensions-inventory.md +40 -0
  96. package/skills/sw-document-feature/reference/scaffold/domains/platform/index.md +39 -0
  97. package/skills/sw-document-feature/reference/scaffold/domains/platform/logging.md +46 -0
  98. package/skills/sw-document-feature/reference/scaffold/guidelines/documentation-guidelines.md +102 -0
  99. package/skills/sw-document-feature/reference/scaffold/guidelines/index.md +50 -0
  100. package/skills/sw-document-feature/reference/scaffold/index.md +68 -0
  101. package/skills/sw-document-feature/reference/scaffold/vanilla/README.md +24 -0
  102. package/skills/sw-document-feature/reference/scaffold/vanilla/adr/index.md +36 -0
  103. package/skills/sw-document-feature/reference/scaffold/vanilla/index.md +68 -0
  104. package/skills/sw-document-feature/reference/setup-wiki.md +142 -0
  105. package/skills/sw-document-feature/reference/source-mapping.md +71 -0
  106. package/skills/sw-document-feature/reference/templates/adr.md +26 -0
  107. package/skills/sw-document-feature/reference/templates/domain-index.md +36 -0
  108. package/skills/sw-document-feature/reference/templates/feature-page.md +64 -0
  109. package/skills/sw-document-feature/reference/templates/guideline.md +42 -0
  110. package/skills/sw-implement-feature/SKILL.md +52 -0
  111. package/skills/sw-implement-feature/reference/ac-loop.md +24 -0
  112. package/skills/sw-implement-feature/reference/build-order.md +17 -0
  113. package/skills/sw-implement-feature/reference/delegation.md +18 -0
  114. package/skills/sw-setup/SKILL.md +144 -0
  115. package/skills/sw-setup/reference/rows.md +223 -0
  116. package/skills/sw-verify-feature/SKILL.md +64 -0
  117. package/skills/sw-verify-feature/reference/reconcile-and-report.md +42 -0
  118. package/skills/sw-verify-feature/reference/verification-report-template.md +57 -0
  119. package/skills/sw-verify-feature-ac-tests/SKILL.md +76 -0
  120. package/skills/sw-verify-feature-ac-tests/reference/audit-execute-score.md +40 -0
  121. package/skills/sw-verify-feature-architecture/SKILL.md +66 -0
  122. package/skills/sw-verify-feature-architecture/reference/guidelines-and-twig.md +27 -0
  123. package/skills/sw-verify-feature-code-quality/SKILL.md +73 -0
  124. package/skills/sw-verify-feature-code-quality/reference/gates.md +22 -0
package/lib/lock.mjs ADDED
@@ -0,0 +1,158 @@
1
+ // The lock file: `.sw-ai-sdk/harness.lock.json` in the host repository.
2
+ //
3
+ // This is the ownership record. JSON config files have no comments, so nothing
4
+ // in `.mcp.json` can tell us who wrote an entry — the lock does, by content
5
+ // hash. It also records what we had to create (files, containers) so that
6
+ // removal is always narrower than addition, and which rules the user declined
7
+ // so we never ask twice.
8
+ //
9
+ // It is gitignored by default: it describes one machine's install, not the
10
+ // project.
11
+
12
+ import { LOCK_FILE, SUPPORTED_CONTENT, CONTENT_VERSION, PKG_NAME, PKG_VERSION } from './content.mjs';
13
+ import { abs, readTextMaybe, writeAtomic } from './fsx.mjs';
14
+
15
+ export class LockVersionError extends Error {}
16
+
17
+ export function lockPath(root) {
18
+ return abs(root, LOCK_FILE);
19
+ }
20
+
21
+ export function empty(scope) {
22
+ return {
23
+ schema: 1,
24
+ content_version: CONTENT_VERSION,
25
+ package: PKG_NAME,
26
+ version: PKG_VERSION,
27
+ scope,
28
+ installed_at: null,
29
+ updated_at: null,
30
+ platform: process.platform,
31
+ hosts: {},
32
+ files: {},
33
+ dirs: [],
34
+ json_pointers: [],
35
+ rules: [],
36
+ fences: [],
37
+ declined: [],
38
+ manual: [],
39
+ };
40
+ }
41
+
42
+ /** Read the lock, or null when absent. Throws on a layout we cannot undo. */
43
+ export function read(root, scope) {
44
+ const found = readTextMaybe(lockPath(root));
45
+ if (found === null) return null;
46
+ let lock;
47
+ try {
48
+ lock = JSON.parse(found.text);
49
+ } catch {
50
+ throw new LockVersionError(
51
+ `${LOCK_FILE} is not valid JSON. Move it aside and re-run apply to rebuild it.`,
52
+ );
53
+ }
54
+ if (!SUPPORTED_CONTENT.includes(lock.content_version)) {
55
+ throw new LockVersionError(
56
+ `${LOCK_FILE} records content_version ${lock.content_version}, which this ` +
57
+ `version of ${PKG_NAME} (${PKG_VERSION}) does not understand.`,
58
+ );
59
+ }
60
+ return { ...empty(scope), ...lock };
61
+ }
62
+
63
+ export function write(root, lock) {
64
+ const next = { ...lock, updated_at: new Date().toISOString() };
65
+ if (!next.installed_at) next.installed_at = next.updated_at;
66
+ writeAtomic(lockPath(root), `${JSON.stringify(next, null, 2)}\n`);
67
+ return next;
68
+ }
69
+
70
+ /** Fold a batch of executed entries into the lock. */
71
+ export function record(lock, entries, { host, version = PKG_VERSION } = {}) {
72
+ const next = {
73
+ ...lock,
74
+ files: { ...lock.files },
75
+ dirs: [...lock.dirs],
76
+ json_pointers: [...lock.json_pointers],
77
+ rules: [...lock.rules],
78
+ fences: [...lock.fences],
79
+ manual: [...lock.manual],
80
+ hosts: { ...lock.hosts },
81
+ };
82
+ for (const entry of entries) {
83
+ if (!entry) continue;
84
+ switch (entry.kind) {
85
+ case 'file-copy':
86
+ next.files[entry.target] = {
87
+ host: entry.host, source: entry.source, hash: entry.hash,
88
+ bytes: entry.bytes, created_file: entry.created_file !== false,
89
+ };
90
+ for (const dir of entry.dirs ?? []) if (!next.dirs.includes(dir)) next.dirs.push(dir);
91
+ break;
92
+ case 'json-merge':
93
+ if (entry.op === 'array-union') {
94
+ upsert(next.rules, (r) => r.file === entry.target && r.pointer === entry.pointer, {
95
+ file: entry.target, pointer: entry.pointer, values: entry.values,
96
+ created_containers: entry.created_containers ?? [],
97
+ created_file: entry.created_file === true,
98
+ });
99
+ } else {
100
+ upsert(next.json_pointers, (p) => p.file === entry.target && p.pointer === entry.pointer, {
101
+ file: entry.target, pointer: entry.pointer, hash: entry.hash,
102
+ created_containers: entry.created_containers ?? [],
103
+ created_file: entry.created_file === true,
104
+ });
105
+ }
106
+ break;
107
+ case 'toml-fence':
108
+ case 'text-fence':
109
+ upsert(next.fences, (f) => f.file === entry.target && f.block === entry.block, {
110
+ file: entry.target, block: entry.block, syntax: entry.syntax,
111
+ hash: entry.hash, created_file: entry.created_file === true,
112
+ });
113
+ break;
114
+ case 'manual':
115
+ upsert(next.manual, (m) => m.file === entry.target && m.block === entry.block, {
116
+ file: entry.target, block: entry.block ?? null,
117
+ remedy: entry.remedy, at: new Date().toISOString(),
118
+ });
119
+ break;
120
+ default:
121
+ break;
122
+ }
123
+ }
124
+ if (host) next.hosts[host] = { installed_at: new Date().toISOString(), version };
125
+ return next;
126
+ }
127
+
128
+ function upsert(list, match, value) {
129
+ const i = list.findIndex(match);
130
+ if (i === -1) list.push(value);
131
+ else list[i] = value;
132
+ }
133
+
134
+ /** A rule the user removed after we added it is a decline, not a gap to refill. */
135
+ export function isDeclined(lock, file, pointer, value) {
136
+ return lock.declined.some((d) => d.file === file && d.pointer === pointer && d.value === value);
137
+ }
138
+
139
+ export function decline(lock, { file, pointer, value, reason }) {
140
+ if (isDeclined(lock, file, pointer, value)) return lock;
141
+ return {
142
+ ...lock,
143
+ declined: [
144
+ ...lock.declined,
145
+ { rule: `${pointer}/${value}`, file, pointer, value, at: new Date().toISOString(), reason },
146
+ ],
147
+ };
148
+ }
149
+
150
+ /** Everything the lock says we own, as a flat list for uninstall. */
151
+ export function entries(lock) {
152
+ return [
153
+ ...Object.entries(lock.files).map(([target, v]) => ({ kind: 'file-copy', target, ...v })),
154
+ ...lock.json_pointers.map((p) => ({ kind: 'json-merge', op: 'set', target: p.file, ...p })),
155
+ ...lock.rules.map((r) => ({ kind: 'json-merge', op: 'array-union', target: r.file, ...r })),
156
+ ...lock.fences.map((f) => ({ kind: f.syntax === 'toml' ? 'toml-fence' : 'text-fence', target: f.file, ...f })),
157
+ ];
158
+ }
package/lib/merge.mjs ADDED
@@ -0,0 +1,338 @@
1
+ // Pure merge primitives. No I/O lives here, which is what makes the risky
2
+ // part of the installer cheap to test exhaustively.
3
+ //
4
+ // Three formats, three ownership strategies:
5
+ //
6
+ // JSON — has no comments, so ownership cannot be written into the file.
7
+ // Each change names an RFC 6901 pointer and the value we put there;
8
+ // the lock file records the pointer and a hash of that value. We
9
+ // never inject an "x-managed-by" key: these files are schema-checked
10
+ // by their hosts and an unknown key is noise at best.
11
+ // TOML — has comments, so a real fence marks our lines and the body is
12
+ // regenerated wholesale. No TOML parser is written; a deliberately
13
+ // narrow line scanner is enough because we only ever replace whole
14
+ // line ranges we ourselves emitted.
15
+ // Text — HTML-comment fence in Markdown, `#` fence in .gitignore. An
16
+ // existing file without a fence is appended to, never rewritten.
17
+
18
+ export const FENCE_ID = 'sw-ecosystem-agentic-harness';
19
+
20
+ // ---------------------------------------------------------------- JSON
21
+
22
+ /**
23
+ * Parse, and report whether the text carries anything that re-serialising
24
+ * would silently destroy. A JSONC file with comments is not ours to rewrite.
25
+ */
26
+ export function parseJsonLoose(text) {
27
+ const hasComments = detectComments(text);
28
+ let value;
29
+ let valid = true;
30
+ try {
31
+ value = JSON.parse(stripBom(text));
32
+ } catch {
33
+ valid = false;
34
+ value = undefined;
35
+ }
36
+ return { value, valid, hasComments };
37
+ }
38
+
39
+ function stripBom(text) {
40
+ return text.startsWith('') ? text.slice(1) : text;
41
+ }
42
+
43
+ /** `//`, `/* *\/` or a trailing comma outside a string literal. */
44
+ function detectComments(text) {
45
+ let inString = false;
46
+ let escaped = false;
47
+ for (let i = 0; i < text.length; i += 1) {
48
+ const c = text[i];
49
+ if (inString) {
50
+ if (escaped) escaped = false;
51
+ else if (c === '\\') escaped = true;
52
+ else if (c === '"') inString = false;
53
+ continue;
54
+ }
55
+ if (c === '"') { inString = true; continue; }
56
+ if (c === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) return true;
57
+ if (c === ',') {
58
+ // A comma followed only by whitespace before a closing bracket is trailing.
59
+ const rest = text.slice(i + 1);
60
+ const m = /^\s*([}\]])/.exec(rest);
61
+ if (m) return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ function decodeToken(token) {
68
+ return token.replace(/~1/g, '/').replace(/~0/g, '~');
69
+ }
70
+
71
+ /** RFC 6901 lookup. `{found, value}` — `found` distinguishes absent from null. */
72
+ export function getPointer(obj, pointer) {
73
+ if (pointer === '') return { found: true, value: obj };
74
+ const tokens = pointer.split('/').slice(1).map(decodeToken);
75
+ let node = obj;
76
+ for (const token of tokens) {
77
+ if (node === null || typeof node !== 'object') return { found: false, value: undefined };
78
+ if (!Object.prototype.hasOwnProperty.call(node, token)) return { found: false, value: undefined };
79
+ node = node[token];
80
+ }
81
+ return { found: true, value: node };
82
+ }
83
+
84
+ /**
85
+ * Set a pointer, creating intermediate objects as needed. Returns the new root
86
+ * plus the pointers of every container it had to create — uninstall prunes
87
+ * only those, so removal is always narrower than addition.
88
+ *
89
+ * The input is not mutated.
90
+ */
91
+ export function setPointer(obj, pointer, value) {
92
+ const tokens = pointer.split('/').slice(1).map(decodeToken);
93
+ const createdContainers = [];
94
+ const root = obj === null || typeof obj !== 'object' || Array.isArray(obj) ? {} : { ...obj };
95
+ let node = root;
96
+ let path = '';
97
+ for (let i = 0; i < tokens.length - 1; i += 1) {
98
+ const token = tokens[i];
99
+ path += `/${token}`;
100
+ const child = node[token];
101
+ if (child === null || typeof child !== 'object' || Array.isArray(child)) {
102
+ if (child === undefined) createdContainers.push(path);
103
+ node[token] = {};
104
+ } else {
105
+ node[token] = { ...child };
106
+ }
107
+ node = node[token];
108
+ }
109
+ const last = tokens[tokens.length - 1];
110
+ node[last] = value;
111
+ return { next: root, createdContainers };
112
+ }
113
+
114
+ /** Remove a pointer, then prune the containers we created, deepest first. */
115
+ export function deletePointer(obj, pointer, createdContainers = []) {
116
+ const clone = JSON.parse(JSON.stringify(obj ?? {}));
117
+ const tokens = pointer.split('/').slice(1).map(decodeToken);
118
+ let node = clone;
119
+ for (let i = 0; i < tokens.length - 1; i += 1) {
120
+ const token = tokens[i];
121
+ if (node === null || typeof node !== 'object') return clone;
122
+ node = node[token];
123
+ if (node === undefined) return clone;
124
+ }
125
+ if (node && typeof node === 'object') delete node[tokens[tokens.length - 1]];
126
+
127
+ for (const container of [...createdContainers].sort((a, b) => b.length - a.length)) {
128
+ const got = getPointer(clone, container);
129
+ if (!got.found || got.value === null || typeof got.value !== 'object') continue;
130
+ if (Object.keys(got.value).length > 0) continue;
131
+ const parts = container.split('/').slice(1).map(decodeToken);
132
+ let parent = clone;
133
+ let ok = true;
134
+ for (let i = 0; i < parts.length - 1; i += 1) {
135
+ parent = parent?.[parts[i]];
136
+ if (parent === undefined) { ok = false; break; }
137
+ }
138
+ if (ok && parent && typeof parent === 'object') delete parent[parts[parts.length - 1]];
139
+ }
140
+ return clone;
141
+ }
142
+
143
+ /**
144
+ * Union into an array of rule strings. Existing elements are never removed,
145
+ * reordered or rewritten — that is directive 4, and the whole reason a user
146
+ * trusts this thing near their settings file.
147
+ */
148
+ export function mergeArrayUnique(existing, additions) {
149
+ const base = Array.isArray(existing) ? [...existing] : [];
150
+ const added = [];
151
+ const alreadyPresent = [];
152
+ for (const value of additions) {
153
+ if (base.includes(value)) alreadyPresent.push(value);
154
+ else { base.push(value); added.push(value); }
155
+ }
156
+ return { next: base, added, alreadyPresent };
157
+ }
158
+
159
+ /** Remove only the values we recorded; anything else in the array survives. */
160
+ export function removeArrayValues(existing, values) {
161
+ const base = Array.isArray(existing) ? [...existing] : [];
162
+ const removed = [];
163
+ for (const value of values) {
164
+ const i = base.indexOf(value);
165
+ if (i !== -1) { base.splice(i, 1); removed.push(value); }
166
+ }
167
+ return { next: base, removed };
168
+ }
169
+
170
+ /** Serialise, restoring the formatting captured on read. */
171
+ export function serializeJson(value, fmt = {}) {
172
+ const indent = fmt.indent ?? ' ';
173
+ let text = JSON.stringify(value, null, indent);
174
+ if (fmt.trailingNewline !== false) text += '\n';
175
+ return text;
176
+ }
177
+
178
+ /** Sorted-key JSON — for hashing only, so key order never looks like drift. */
179
+ export function canonical(value) {
180
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
181
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
182
+ const keys = Object.keys(value).sort();
183
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
184
+ }
185
+
186
+ // ------------------------------------------------------- fences (TOML/text)
187
+
188
+ const SYNTAX = {
189
+ toml: {
190
+ open: (block, meta) => `# >>> managed by ${FENCE_ID} block=${block} v=${meta.version}`,
191
+ close: (block) => `# <<< end managed by ${FENCE_ID} block=${block}`,
192
+ warn: `# Do not edit inside this fence — \`${FENCE_ID} apply\` rewrites it.`,
193
+ },
194
+ markdown: {
195
+ open: (block, meta) => `<!-- >>> managed by ${FENCE_ID} block=${block} v=${meta.version} -->`,
196
+ close: (block) => `<!-- <<< end managed by ${FENCE_ID} block=${block} -->`,
197
+ warn: null,
198
+ },
199
+ gitignore: {
200
+ open: (block, meta) => `# >>> managed by ${FENCE_ID} block=${block} v=${meta.version}`,
201
+ close: (block) => `# <<< end managed by ${FENCE_ID} block=${block}`,
202
+ warn: null,
203
+ },
204
+ };
205
+
206
+ function openRe(block) {
207
+ return new RegExp(`(^|\\s)managed by ${FENCE_ID} block=${escapeRe(block)}(\\s|$)`);
208
+ }
209
+ function closeRe(block) {
210
+ return new RegExp(`(^|\\s)end managed by ${FENCE_ID} block=${escapeRe(block)}(\\s|$)`);
211
+ }
212
+ function escapeRe(s) {
213
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
214
+ }
215
+
216
+ /**
217
+ * Locate a fence. Returns line indices and the inner body, or null.
218
+ * A close marker is matched before an open one so `end managed by` never
219
+ * reads as an opening line.
220
+ */
221
+ export function findFence(text, block) {
222
+ const lines = text.split('\n');
223
+ const open = openRe(block);
224
+ const close = closeRe(block);
225
+ let start = -1;
226
+ for (let i = 0; i < lines.length; i += 1) {
227
+ if (close.test(lines[i])) {
228
+ if (start !== -1) {
229
+ return { start, end: i, inner: lines.slice(start + 1, i).join('\n') };
230
+ }
231
+ continue;
232
+ }
233
+ if (start === -1 && open.test(lines[i])) start = i;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * Exactly what sits between the two fence markers. `findFence` reads this back,
240
+ * so hashing it — rather than the caller's body — is what makes "has the user
241
+ * edited inside the fence?" a reliable question.
242
+ */
243
+ export function fenceInner(body, syntax = 'toml') {
244
+ const s = SYNTAX[syntax];
245
+ if (!s) throw new Error(`unknown fence syntax: ${syntax}`);
246
+ return s.warn ? `${s.warn}\n${body}` : body;
247
+ }
248
+
249
+ /** Create or replace a fence. Everything outside it stays byte-identical. */
250
+ export function upsertFence(text, block, body, meta = {}, syntax = 'toml') {
251
+ const s = SYNTAX[syntax];
252
+ if (!s) throw new Error(`unknown fence syntax: ${syntax}`);
253
+ const version = meta.version ?? '0.0.0';
254
+ const fence = [
255
+ s.open(block, { version }),
256
+ ...fenceInner(body, syntax).split('\n'),
257
+ s.close(block),
258
+ ].join('\n');
259
+
260
+ const existing = findFence(text ?? '', block);
261
+ if (existing) {
262
+ const lines = (text ?? '').split('\n');
263
+ const next = [...lines.slice(0, existing.start), ...fence.split('\n'), ...lines.slice(existing.end + 1)].join('\n');
264
+ return { next, changed: next !== text, created: false };
265
+ }
266
+ if (!text || text.trim() === '') return { next: `${fence}\n`, changed: true, created: true };
267
+ const sep = text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
268
+ const next = `${text}${sep}${fence}\n`;
269
+ return { next, changed: true, created: false };
270
+ }
271
+
272
+ /**
273
+ * Remove a fence, restoring the file to what it was before we wrote it.
274
+ *
275
+ * Exact, with one documented exception: a file that had no final newline gets
276
+ * one when we append to it, and nothing in the file records that it was
277
+ * missing, so it keeps the newline afterwards. Adding a final newline to a
278
+ * text file is a normalisation, not a loss.
279
+ */
280
+ export function removeFence(text, block) {
281
+ const existing = findFence(text ?? '', block);
282
+ if (!existing) return { next: text, changed: false };
283
+ const lines = (text ?? '').split('\n');
284
+ const before = lines.slice(0, existing.start);
285
+ const after = lines.slice(existing.end + 1);
286
+ // The blank line we inserted when appending goes with the fence.
287
+ while (before.length && before[before.length - 1].trim() === '' && after.length === 0) before.pop();
288
+ while (before.length && after.length && before[before.length - 1].trim() === '' && after[0].trim() === '') before.pop();
289
+ const next = [...before, ...after].join('\n');
290
+ return { next, changed: true };
291
+ }
292
+
293
+ /**
294
+ * Whether a TOML table header exists outside any of our fences. TOML forbids
295
+ * duplicate headers, so one of these means we must not emit ours.
296
+ */
297
+ export function tomlTableExists(text, header, ourBlocks = []) {
298
+ if (!text) return false;
299
+ const lines = text.split('\n');
300
+ const masked = new Set();
301
+ for (const block of ourBlocks) {
302
+ const fence = findFence(text, block);
303
+ if (!fence) continue;
304
+ for (let i = fence.start; i <= fence.end; i += 1) masked.add(i);
305
+ }
306
+ const target = header.trim();
307
+ for (let i = 0; i < lines.length; i += 1) {
308
+ if (masked.has(i)) continue;
309
+ const line = lines[i];
310
+ // Table headers sit at column 0. Anything indented is inside a value.
311
+ if (/^\[/.test(line) && line.split('#')[0].trim() === target) return true;
312
+ }
313
+ return false;
314
+ }
315
+
316
+ /** A TOML basic string. Values are pre-resolved; no `${VAR}` is ever emitted. */
317
+ export function tomlValue(value) {
318
+ if (Array.isArray(value)) return `[${value.map(tomlValue).join(', ')}]`;
319
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
320
+ return JSON.stringify(String(value));
321
+ }
322
+
323
+ /** Render `[header]` plus flat key/values. */
324
+ export function renderTomlTable(header, kv) {
325
+ const lines = [header];
326
+ for (const [key, value] of Object.entries(kv)) {
327
+ if (value === undefined || value === null) continue;
328
+ if (typeof value === 'object' && !Array.isArray(value)) {
329
+ const inner = Object.entries(value)
330
+ .map(([k, v]) => `${k} = ${tomlValue(v)}`)
331
+ .join(', ');
332
+ lines.push(`${key} = { ${inner} }`);
333
+ } else {
334
+ lines.push(`${key} = ${tomlValue(value)}`);
335
+ }
336
+ }
337
+ return lines.join('\n');
338
+ }
package/lib/out.mjs ADDED
@@ -0,0 +1,74 @@
1
+ // Output contract shared by every sw-ecosystem-agentic-harness subcommand.
2
+ //
3
+ // stdout carries exactly one JSON object and nothing else. Its keys are
4
+ // ordered small-first with the big payload LAST, so an agent that reads only
5
+ // the head of the output already knows whether it worked and what to run next.
6
+ //
7
+ // stderr carries progress and warnings that are not part of the result.
8
+ //
9
+ // The contract: structured JSON on stdout, a mandatory `next_step` on every
10
+ // success, `exit 2` on a usage error carrying a `help` array of runnable
11
+ // commands.
12
+
13
+ export const EXIT_OK = 0;
14
+ export const EXIT_FAIL = 1;
15
+ export const EXIT_USAGE = 2;
16
+
17
+ /** Serialise with a stable shape: two-space indent, trailing newline. */
18
+ export function json(value) {
19
+ return `${JSON.stringify(value, null, 2)}\n`;
20
+ }
21
+
22
+ function emit(body, code) {
23
+ process.stdout.write(json(body));
24
+ process.exitCode = code;
25
+ return code;
26
+ }
27
+
28
+ /**
29
+ * A successful result. `body.next_step` is mandatory and enforced here rather
30
+ * than in review: a result an agent cannot act on is a bug, not a style issue.
31
+ */
32
+ export function ok(command, body) {
33
+ if (!body || typeof body.next_step !== 'string' || body.next_step.trim() === '') {
34
+ throw new Error(`${command}: every successful result must carry a next_step`);
35
+ }
36
+ return emit({ ok: true, command, ...body }, EXIT_OK);
37
+ }
38
+
39
+ /**
40
+ * The invocation was wrong — an unknown command, a missing `--yes`, a bad
41
+ * flag value. `help` is a list of runnable commands, not prose.
42
+ */
43
+ export function usage(command, error, help) {
44
+ return emit(
45
+ {
46
+ ok: false,
47
+ command: command || null,
48
+ error,
49
+ next_step: help && help.length ? help[0] : 'Run `sw-ecosystem-agentic-harness guide`.',
50
+ help: help && help.length ? help : ['sw-ecosystem-agentic-harness guide'],
51
+ },
52
+ EXIT_USAGE,
53
+ );
54
+ }
55
+
56
+ /** The invocation was fine but the work could not be done. */
57
+ export function failure(command, error, body = {}) {
58
+ return emit(
59
+ {
60
+ ok: false,
61
+ command,
62
+ error,
63
+ next_step: body.next_step || 'Fix the error above, then re-run the command.',
64
+ help: body.help || [`sw-ecosystem-agentic-harness status`],
65
+ ...body,
66
+ },
67
+ EXIT_FAIL,
68
+ );
69
+ }
70
+
71
+ /** Progress or a warning that is not part of the result. */
72
+ export function note(text) {
73
+ process.stderr.write(`${text}\n`);
74
+ }
package/mcp.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "playwright": {
5
+ "type": "stdio",
6
+ "command": "npx",
7
+ "args": ["-y", "@playwright/mcp@latest"]
8
+ }
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@execuro-sw-ecosystem/sw-ecosystem-agentic-harness",
3
+ "version": "0.1.0",
4
+ "description": "Shopware 6 agentic harness: sw-* skills and sub-agents, plus an installer that writes them into Claude Code, Codex, GitHub Copilot and Cursor.",
5
+ "license": "MIT",
6
+ "author": "Execuro UG (haftungsbeschränkt)",
7
+ "homepage": "https://github.com/execuro/sw-ecosystem-agentic-harness#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/execuro/sw-ecosystem-agentic-harness.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/execuro/sw-ecosystem-agentic-harness/issues"
14
+ },
15
+ "keywords": [
16
+ "shopware",
17
+ "agentic",
18
+ "claude-code",
19
+ "codex",
20
+ "copilot",
21
+ "cursor",
22
+ "skills",
23
+ "subagents"
24
+ ],
25
+ "type": "module",
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "bin": {
30
+ "sw-ecosystem-agentic-harness": "bin/cli.mjs"
31
+ },
32
+ "files": [
33
+ "bin/",
34
+ "lib/",
35
+ "agents/",
36
+ "skills/",
37
+ "codex/",
38
+ "copilot/",
39
+ "content/",
40
+ "plugin.json",
41
+ "mcp.json",
42
+ ".mcp.json",
43
+ "README.md",
44
+ "CHANGELOG.md",
45
+ "LICENSE",
46
+ "SECURITY.md",
47
+ "CONTRIBUTING.md"
48
+ ],
49
+ "scripts": {
50
+ "gen": "node scripts/gen-codex-agents.mjs && node scripts/gen-copilot-agents.mjs && node scripts/gen-agents-md.mjs",
51
+ "gen:check": "node scripts/gen-codex-agents.mjs --check && node scripts/gen-copilot-agents.mjs --check && node scripts/gen-agents-md.mjs --check",
52
+ "test": "node --test test/*.test.mjs scripts/*.test.mjs",
53
+ "check:mcp": "node scripts/check-mcp.mjs"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }
package/plugin.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "sw-ecosystem-agentic-harness",
4
+ "version": "0.1.0",
5
+ "description": "Shopware 6 agentic harness: sw-* skills and sub-agents for requirements, specs, implementation, verification and documentation, with an installer CLI that writes them into Claude Code, Codex, GitHub Copilot and Cursor. The knowledge-base MCP and the editor tools are separate npm packages.",
6
+ "author": {
7
+ "name": "Execuro UG (haftungsbeschränkt)",
8
+ "url": "https://github.com/execuro"
9
+ },
10
+ "homepage": "https://github.com/execuro/sw-ecosystem-agentic-harness",
11
+ "repository": "https://github.com/execuro/sw-ecosystem-agentic-harness",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "shopware",
15
+ "php",
16
+ "twig",
17
+ "vue"
18
+ ]
19
+ }