@jenga-ai/agent 1.0.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 (177) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +340 -0
  3. package/agents/ai_engineer.md +113 -0
  4. package/agents/developer.md +236 -0
  5. package/agents/scrum-master.md +349 -0
  6. package/agents/scrutiny-agent.md +137 -0
  7. package/agents/solution-assessor.md +185 -0
  8. package/agents/tester.md +339 -0
  9. package/bin/jenga.js +70 -0
  10. package/hooks/copilot_session_end.sh +29 -0
  11. package/hooks/on_session_end.sh +238 -0
  12. package/hooks/prompt_router.sh +11 -0
  13. package/hooks/prompt_router_helper.js +52 -0
  14. package/hooks/session_end_helper.js +29 -0
  15. package/hooks/session_end_watcher.sh +24 -0
  16. package/lib/commands/attach.js +47 -0
  17. package/lib/commands/init.js +207 -0
  18. package/lib/commands/start.js +16 -0
  19. package/lib/commands/status.js +53 -0
  20. package/lib/config-schema.js +72 -0
  21. package/lib/inject-settings.js +61 -0
  22. package/lib/mirror.js +244 -0
  23. package/lib/resolve-project-dir.sh +47 -0
  24. package/mcp/execute-ticket/index.js +10 -0
  25. package/mcp/execute-ticket/package.json +5 -0
  26. package/mcp/help/index.js +79 -0
  27. package/mcp/help/package.json +14 -0
  28. package/mcp/router/README.md +19 -0
  29. package/mcp/router/embedder.js +23 -0
  30. package/mcp/router/index.js +204 -0
  31. package/mcp/router/matcher.js +87 -0
  32. package/mcp/router/package-lock.json +1048 -0
  33. package/mcp/router/package.json +11 -0
  34. package/mcp/router/skill-index.js +104 -0
  35. package/package.json +47 -0
  36. package/scripts/board_resolver.sh +46 -0
  37. package/scripts/e25_s01_extract_board_graph.py +292 -0
  38. package/scripts/e25_s01_generate_synthetic_board.py +90 -0
  39. package/scripts/measurement-10x.json +50 -0
  40. package/scripts/measurement-10x.txt +4 -0
  41. package/scripts/measurement-real.json +50 -0
  42. package/scripts/measurement-real.txt +4 -0
  43. package/scripts/postinstall.js +165 -0
  44. package/scripts/todo_cleanup.sh +22 -0
  45. package/scripts/todo_manager.sh +86 -0
  46. package/scripts/validate-board.sh +190 -0
  47. package/scripts/validate-story-format.sh +53 -0
  48. package/skills/brainstorm/SKILL.md +47 -0
  49. package/skills/btw/SKILL.md +42 -0
  50. package/skills/commit/SKILL.md +29 -0
  51. package/skills/commit/assets/user_instructions_template.md +22 -0
  52. package/skills/continue/SKILL.md +29 -0
  53. package/skills/convert/SKILL.md +124 -0
  54. package/skills/convert/convert_cli.py +235 -0
  55. package/skills/convert/tests/sample.csv +4 -0
  56. package/skills/convert/tests/sample.json +5 -0
  57. package/skills/convert/tests/sample.jsonl +3 -0
  58. package/skills/convert/tests/sample.yaml +18 -0
  59. package/skills/convert/tests/sample_obj.csv +2 -0
  60. package/skills/convert/tests/sample_obj.json +9 -0
  61. package/skills/deep-dive/SKILL.md +167 -0
  62. package/skills/do/SKILL.md +88 -0
  63. package/skills/do/assets/sender_template.json +12 -0
  64. package/skills/doc/SKILL.md +314 -0
  65. package/skills/doc/assets/path-objectives.yaml +38 -0
  66. package/skills/doc-sync/SKILL.md +167 -0
  67. package/skills/doc-sync/assets/default_excludes.txt +21 -0
  68. package/skills/doc-sync/assets/doc_targets.md +14 -0
  69. package/skills/dooo/SKILL.md +60 -0
  70. package/skills/error/SKILL.md +29 -0
  71. package/skills/evaluate/SKILL.md +45 -0
  72. package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
  73. package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
  74. package/skills/examplify/SKILL.md +42 -0
  75. package/skills/help/SKILL.md +36 -0
  76. package/skills/improve/SKILL.md +55 -0
  77. package/skills/index/scripts/board-index +4 -0
  78. package/skills/index/scripts/board_index.py +615 -0
  79. package/skills/index/scripts/smoke_test.sh +86 -0
  80. package/skills/init/SKILL.md +44 -0
  81. package/skills/init/assets/.gitignore_template +15 -0
  82. package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
  83. package/skills/init/assets/directory_structure.txt +13 -0
  84. package/skills/init/assets/test-config_template.json +4 -0
  85. package/skills/init/assets/workflow_template.json +30 -0
  86. package/skills/init/scripts/init.sh +48 -0
  87. package/skills/jbp/SKILL.md +25 -0
  88. package/skills/jenga/SKILL.md +68 -0
  89. package/skills/lgtm/SKILL.md +21 -0
  90. package/skills/mirror-public/SKILL.md +237 -0
  91. package/skills/mirror-public/assets/config.json +5 -0
  92. package/skills/mirror-public/scripts/mirror.sh +374 -0
  93. package/skills/pi-plan/SKILL.md +62 -0
  94. package/skills/pi-plan/assets/epic.json +7 -0
  95. package/skills/pi-plan/assets/story_template.md +18 -0
  96. package/skills/proceed/SKILL.md +29 -0
  97. package/skills/publish/SKILL.md +351 -0
  98. package/skills/publish/adapters/droplet.md +200 -0
  99. package/skills/publish/adapters/mobile-ios.md +114 -0
  100. package/skills/publish/adapters/npm-ci.md +223 -0
  101. package/skills/publish/adapters/npm.md +121 -0
  102. package/skills/publish/assets/ExportOptions.plist.template +19 -0
  103. package/skills/publish/assets/ci-contract.md +111 -0
  104. package/skills/publish/assets/ownership-matrix.md +17 -0
  105. package/skills/publish/assets/publish.example.json +85 -0
  106. package/skills/publish/assets/publish.example.npm-ci.json +40 -0
  107. package/skills/publish/assets/publish.example.npm.json +41 -0
  108. package/skills/publish/assets/secrets-guide.md +104 -0
  109. package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
  110. package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
  111. package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
  112. package/skills/publish/schemas/publish.schema.json +428 -0
  113. package/skills/publish/scripts/check_target_config.sh +96 -0
  114. package/skills/publish/scripts/droplet_pipeline.sh +208 -0
  115. package/skills/publish/scripts/generate_release_notes.sh +200 -0
  116. package/skills/publish/scripts/ios_pipeline.sh +486 -0
  117. package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
  118. package/skills/publish/scripts/npm_pipeline.sh +249 -0
  119. package/skills/publish/scripts/publish_common.sh +253 -0
  120. package/skills/publish/scripts/publish_deploy.sh +538 -0
  121. package/skills/publish/scripts/reconcile_tags.sh +135 -0
  122. package/skills/publish/scripts/run_gates.sh +616 -0
  123. package/skills/publish/scripts/setup_wizard.sh +394 -0
  124. package/skills/publish/scripts/show_history.sh +95 -0
  125. package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
  126. package/skills/publish/scripts/validate_config.sh +163 -0
  127. package/skills/publish/scripts/validate_droplet_env.sh +45 -0
  128. package/skills/publish/scripts/validate_ios_env.sh +68 -0
  129. package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
  130. package/skills/publish/scripts/validate_npm_env.sh +22 -0
  131. package/skills/publish/scripts/write_ledger_entry.sh +126 -0
  132. package/skills/publish/wizards/droplet.md +275 -0
  133. package/skills/publish/wizards/mobile-ios.md +157 -0
  134. package/skills/publish/wizards/npm-ci.md +240 -0
  135. package/skills/publish/wizards/npm.md +224 -0
  136. package/skills/reconcile/SKILL.md +93 -0
  137. package/skills/reconcile/assets/report_format.md +44 -0
  138. package/skills/reconcile-origin/SKILL.md +75 -0
  139. package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
  140. package/skills/redo/SKILL.md +70 -0
  141. package/skills/route/SKILL.md +180 -0
  142. package/skills/self-sync/SKILL.md +73 -0
  143. package/skills/self-sync/scripts/run.js +136 -0
  144. package/skills/skillify/SKILL.md +68 -0
  145. package/skills/skillify/assets/init-new/SKILL.md +35 -0
  146. package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
  147. package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
  148. package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
  149. package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
  150. package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
  151. package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
  152. package/skills/skillify/assets/init-old/SKILL.md +124 -0
  153. package/skills/spinoff/SKILL.md +48 -0
  154. package/skills/status/SKILL.md +33 -0
  155. package/skills/status/assets/output_format.md +41 -0
  156. package/skills/todo/SKILL.md +46 -0
  157. package/skills/todo/assets/todo_handoff_template.md +22 -0
  158. package/skills/todo/assets/todo_template.md +3 -0
  159. package/skills/train/SKILL.md +116 -0
  160. package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
  161. package/skills/train/assets/dashboard-templates/nlp.html +102 -0
  162. package/skills/train/assets/dashboard-templates/transformers.html +98 -0
  163. package/skills/train/assets/results-parsers/__init__.py +9 -0
  164. package/skills/train/assets/results-parsers/classifiers.py +84 -0
  165. package/skills/train/assets/results-parsers/nlp.py +88 -0
  166. package/skills/train/assets/results-parsers/reporter.py +154 -0
  167. package/skills/train/assets/results-parsers/transformers.py +120 -0
  168. package/skills/train/train_cli.py +786 -0
  169. package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
  170. package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
  171. package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
  172. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
  173. package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
  174. package/templates/SKILL.md +16 -0
  175. package/templates/SKILL_TEMPLATE.md +28 -0
  176. package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
  177. package/templates/copilot-instructions.md.tpl +55 -0
@@ -0,0 +1,50 @@
1
+ {
2
+ "results": [
3
+ {
4
+ "command": "python3 scripts/e25_s01_extract_board_graph.py project >/dev/null",
5
+ "mean": 0.13581702562,
6
+ "stddev": 0.01795068160628955,
7
+ "median": 0.12633394632,
8
+ "user": 0.0488429,
9
+ "system": 0.02490142,
10
+ "min": 0.12153511332000001,
11
+ "max": 0.16625157132,
12
+ "times": [
13
+ 0.12962952932000002,
14
+ 0.12303836332,
15
+ 0.16625157132,
16
+ 0.15362957132000002,
17
+ 0.12211615531999999,
18
+ 0.12262336332,
19
+ 0.12190103032,
20
+ 0.16218352932000002,
21
+ 0.13526202932,
22
+ 0.12153511332000001
23
+ ],
24
+ "memory_usage_byte": [
25
+ 20250624,
26
+ 20250624,
27
+ 20267008,
28
+ 20267008,
29
+ 20267008,
30
+ 20267008,
31
+ 20267008,
32
+ 20267008,
33
+ 20430848,
34
+ 20430848
35
+ ],
36
+ "exit_codes": [
37
+ 0,
38
+ 0,
39
+ 0,
40
+ 0,
41
+ 0,
42
+ 0,
43
+ 0,
44
+ 0,
45
+ 0,
46
+ 0
47
+ ]
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,4 @@
1
+ Benchmark 1: python3 scripts/e25_s01_extract_board_graph.py project >/dev/null
2
+ Time (mean ± σ): 135.8 ms ± 18.0 ms [User: 48.8 ms, System: 24.9 ms]
3
+ Range (min … max): 121.5 ms … 166.3 ms 10 runs
4
+
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall.js — JengaAgent consumer installation hook
4
+ *
5
+ * Runs automatically when a consumer project installs jenga-agent via npm.
6
+ * Copies the discovery-bound dirs — `skills/` and `agents/` — into BOTH
7
+ * `<consumer>/.claude/` (where Claude Code looks) and `<consumer>/.agents/`
8
+ * (where non-Claude agents like Copilot / custom look, per the framework's
9
+ * agent-target convention). This duplication is required because each agent
10
+ * ecosystem has its own fixed discovery path.
11
+ *
12
+ * Everything else (`hooks/`, `scripts/`, `templates/`, `mcp/`, `lib/`) stays
13
+ * in the installed package and is sourced from `node_modules/jenga-agent/…`
14
+ * at runtime — no duplication.
15
+ *
16
+ * The consumer's `project/` directory (if present) is left untouched.
17
+ *
18
+ * Safe upgrade behaviour:
19
+ * - First-time installs: always copy all files.
20
+ * - Subsequent installs: only overwrite if the new package version is strictly
21
+ * newer (semver) than the version recorded in <consumer-root>/.jenga-version.
22
+ *
23
+ * After a successful copy, writes .jenga-version with the current package version.
24
+ *
25
+ * The actual filesystem mirror is delegated to `lib/mirror.js`, which is the
26
+ * single source of truth shared with the in-repo `/self-sync` skill. Consumer
27
+ * install always uses `reconcileDeletes: false` (additive-only).
28
+ */
29
+
30
+ import fs from 'node:fs';
31
+ import path from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+
34
+ import { mirror } from '../lib/mirror.js';
35
+
36
+ // ESM equivalent of __dirname
37
+ const __filename = fileURLToPath(import.meta.url);
38
+ const __dirname = path.dirname(__filename);
39
+
40
+ // ── helpers ──────────────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Very small semver comparator.
44
+ * Returns 1 if a > b, -1 if a < b, 0 if equal.
45
+ */
46
+ function semverCompare(a, b) {
47
+ const clean = (v) => String(v || '0.0.0').replace(/[^0-9.]/g, '');
48
+ const parts = (v) => clean(v).split('.').map(Number);
49
+ const [aMaj, aMin, aPat] = parts(a);
50
+ const [bMaj, bMin, bPat] = parts(b);
51
+ if (aMaj !== bMaj) return aMaj > bMaj ? 1 : -1;
52
+ if (aMin !== bMin) return aMin > bMin ? 1 : -1;
53
+ if (aPat !== bPat) return aPat > bPat ? 1 : -1;
54
+ return 0;
55
+ }
56
+
57
+ // ── main ─────────────────────────────────────────────────────────────────────
58
+
59
+ function main() {
60
+ // __dirname is scripts/ inside the package; package root is one level up
61
+ const packageRoot = path.resolve(__dirname, '..');
62
+
63
+ // npm sets INIT_CWD to the directory where the user ran `npm install`
64
+ const consumerRoot = process.env.INIT_CWD || process.cwd();
65
+
66
+ // Avoid running during development (when INIT_CWD === packageRoot itself)
67
+ if (path.resolve(consumerRoot) === path.resolve(packageRoot)) {
68
+ console.log('\n ℹ JengaAgent postinstall: running inside the package itself — skipping copy.\n');
69
+ return;
70
+ }
71
+
72
+ // Read this package's version
73
+ let packageVersion = '0.0.0';
74
+ try {
75
+ const pkgPath = path.join(packageRoot, 'package.json');
76
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
77
+ packageVersion = pkg.version || '0.0.0';
78
+ } catch (_) {
79
+ // non-fatal — proceed with default
80
+ }
81
+
82
+ // Read the installed version at the consumer root (if any)
83
+ const versionFile = path.join(consumerRoot, '.jenga-version');
84
+ let installedVersion = null;
85
+ try {
86
+ installedVersion = fs.readFileSync(versionFile, 'utf8').trim();
87
+ } catch (_) {
88
+ // file doesn't exist yet — first-time install
89
+ }
90
+
91
+ const isFirstInstall = installedVersion === null;
92
+ const isUpgrade = !isFirstInstall && semverCompare(packageVersion, installedVersion) > 0;
93
+ const shouldCopy = isFirstInstall || isUpgrade;
94
+
95
+ console.log('\n╔══════════════════════════════════════════════════════╗');
96
+ console.log('║ JengaAgent — postinstall hook ║');
97
+ console.log('╚══════════════════════════════════════════════════════╝\n');
98
+ console.log(` Package version : ${packageVersion}`);
99
+ console.log(` Installed version: ${installedVersion ?? '(none — first install)'}`);
100
+ console.log(` Consumer root : ${consumerRoot}`);
101
+ console.log(` Action : ${
102
+ isFirstInstall ? 'First install — copying all framework files' :
103
+ isUpgrade ? `Upgrade ${installedVersion} → ${packageVersion} — copying all framework files` :
104
+ 'Same or newer version already installed — skipping overwrite'
105
+ }\n`);
106
+
107
+ if (!shouldCopy) {
108
+ console.log(' ✓ Nothing to do. Your framework files are already up to date.\n');
109
+ return;
110
+ }
111
+
112
+ // Discovery-bound dirs get mirrored to both .claude/ (Claude Code) and
113
+ // .agents/ (Copilot / custom). Everything else is sourced from
114
+ // node_modules/jenga-agent/… at runtime — no duplication.
115
+ const copySet = ['skills', 'agents'];
116
+ const targetRoots = ['.claude', '.agents'];
117
+
118
+ // Warn about any copySet entries that are missing from the package — the
119
+ // helper silently skips missing sources, so we surface it here for parity
120
+ // with the previous UX.
121
+ for (const entry of copySet) {
122
+ if (!fs.existsSync(path.join(packageRoot, entry))) {
123
+ console.log(` ⚠ ${entry}/ not found in package — skipped`);
124
+ }
125
+ }
126
+
127
+ let totalCopied = 0;
128
+ let totalSkipped = 0;
129
+
130
+ for (const targetRoot of targetRoots) {
131
+ const destRoot = path.join(consumerRoot, targetRoot);
132
+
133
+ // Consumer install is additive-only: never delete files from the mirror.
134
+ const result = mirror({
135
+ sourceRoot: packageRoot,
136
+ destRoot,
137
+ copySet,
138
+ dryRun: false,
139
+ reconcileDeletes: false,
140
+ });
141
+
142
+ // For the per-directory line we report the aggregate write count
143
+ // (added + overwritten) so the message matches the pre-refactor UX
144
+ // ("N file(s) copied") even though we now distinguish the two.
145
+ for (const entry of copySet) {
146
+ const wrote =
147
+ result.added.filter((p) => p.startsWith(path.join(destRoot, entry))).length +
148
+ result.overwritten.filter((p) => p.startsWith(path.join(destRoot, entry))).length;
149
+ console.log(` ✓ ${targetRoot}/${entry}/ — ${wrote} file(s) copied`);
150
+ }
151
+
152
+ totalCopied += result.added.length + result.overwritten.length;
153
+ totalSkipped += result.skipped.length;
154
+ }
155
+
156
+ // Write .jenga-version to record the installed version at consumer root
157
+ fs.writeFileSync(versionFile, packageVersion + '\n', 'utf8');
158
+
159
+ console.log('\n──────────────────────────────────────────────────────');
160
+ console.log(` Summary: ${totalCopied} file(s) copied, ${totalSkipped} skipped`);
161
+ console.log(` .jenga-version written: ${packageVersion}`);
162
+ console.log('──────────────────────────────────────────────────────\n');
163
+ }
164
+
165
+ main();
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+
3
+ TODO_FILE="project/todo.md"
4
+
5
+ [ ! -f "$TODO_FILE" ] && exit 0
6
+
7
+ # Check if file is effectively empty (only blank lines, "# Todo", or single-line HTML comments)
8
+ while IFS= read -r line; do
9
+ # Skip blank/whitespace-only lines
10
+ [[ -z "${line// }" ]] && continue
11
+ # Skip "# Todo" header
12
+ [[ "$line" == "# Todo" ]] && continue
13
+ # Skip single-line HTML comments
14
+ [[ "$line" == \<\!--*--\> ]] && continue
15
+ # Found a real entry — exit silently
16
+ exit 0
17
+ done < "$TODO_FILE"
18
+
19
+ # File is effectively empty — delete it
20
+ rm "$TODO_FILE"
21
+ echo "[todo_cleanup] project/todo.md was empty — deleted."
22
+ exit 0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/todo_manager.sh — canonical owner of all project/todo.md operations.
3
+ # Run from the repository root.
4
+
5
+ TODO_FILE="project/todo.md"
6
+ TEMPLATE="skills/todo/assets/todo_template.md"
7
+
8
+ usage() {
9
+ cat >&2 <<EOF
10
+ Usage: $0 <subcommand> [args]
11
+
12
+ Subcommands:
13
+ add "<entry>" Append entry to project/todo.md (auto-creates from template if missing)
14
+ remove "<title>" Remove first line containing <title> (exits non-zero if not found)
15
+ list Print all non-comment, non-blank entries (silent if file missing/empty)
16
+ exists Exit 0 if at least one real entry exists; exit 1 otherwise
17
+ teardown Delete project/todo.md if effectively empty; no-op if absent or has real entries
18
+ EOF
19
+ exit 1
20
+ }
21
+
22
+ # Filters out blank lines, lines starting with #, and HTML comments
23
+ real_entries() {
24
+ grep -v '^\s*$' "$1" 2>/dev/null \
25
+ | grep -v '^\s*#' \
26
+ | grep -v '^\s*<!--'
27
+ }
28
+
29
+ case "${1:-}" in
30
+ add)
31
+ [ -z "${2:-}" ] && { echo "Error: add requires an entry argument" >&2; exit 1; }
32
+ if [ ! -f "$TODO_FILE" ]; then
33
+ if [ ! -f "$TEMPLATE" ]; then
34
+ echo "Error: template not found at $TEMPLATE" >&2; exit 1
35
+ fi
36
+ cp "$TEMPLATE" "$TODO_FILE"
37
+ fi
38
+ printf '%s\n' "$2" >> "$TODO_FILE"
39
+ ;;
40
+
41
+ remove)
42
+ [ -z "${2:-}" ] && { echo "Error: remove requires a title argument" >&2; exit 1; }
43
+ if [ ! -f "$TODO_FILE" ]; then
44
+ echo "Error: $TODO_FILE does not exist" >&2; exit 1
45
+ fi
46
+ # Check for at least one match
47
+ if ! grep -qF "$2" "$TODO_FILE"; then
48
+ echo "Error: no line matching \"$2\" found in $TODO_FILE" >&2; exit 1
49
+ fi
50
+ # Remove only the FIRST matching line atomically
51
+ awk -v title="$2" 'found || !index($0, title) { print; next } { found=1 }' \
52
+ "$TODO_FILE" > "${TODO_FILE}.tmp" && mv "${TODO_FILE}.tmp" "$TODO_FILE"
53
+ ;;
54
+
55
+ list)
56
+ [ ! -f "$TODO_FILE" ] && exit 0
57
+ real_entries "$TODO_FILE"
58
+ exit 0
59
+ ;;
60
+
61
+ exists)
62
+ if [ ! -f "$TODO_FILE" ]; then
63
+ echo "todo.md is missing" >&2; exit 1
64
+ fi
65
+ if [ -z "$(real_entries "$TODO_FILE")" ]; then
66
+ echo "todo.md exists but has no real entries" >&2; exit 1
67
+ fi
68
+ exit 0
69
+ ;;
70
+
71
+ teardown)
72
+ [ ! -f "$TODO_FILE" ] && exit 0
73
+ # "Effectively empty" = only blank lines, "# Todo", or HTML comments
74
+ non_trivial=$(grep -v '^\s*$' "$TODO_FILE" \
75
+ | grep -v '^\s*# Todo\s*$' \
76
+ | grep -v '^\s*<!--')
77
+ if [ -z "$non_trivial" ]; then
78
+ rm -f "$TODO_FILE"
79
+ fi
80
+ exit 0
81
+ ;;
82
+
83
+ *)
84
+ usage
85
+ ;;
86
+ esac
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+ # validate-board.sh — validate scrum board frontmatter for epic, story, and task files
3
+ #
4
+ # Usage: ./scripts/validate-board.sh <path-to-board-file> [more-files...]
5
+
6
+ set -euo pipefail
7
+
8
+ if [[ "$#" -lt 1 ]]; then
9
+ echo "❌ Usage: $(basename "$0") <path-to-board-file> [more-files...]"
10
+ exit 1
11
+ fi
12
+
13
+ python3 - "$@" <<'PY'
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ STATUS_VALUES = {
19
+ "Pending",
20
+ "In Progress",
21
+ "Passed",
22
+ "Passed with remarks",
23
+ "Failed",
24
+ "Rejected",
25
+ "Blocked",
26
+ "Backlog",
27
+ "Done",
28
+ }
29
+
30
+ ALLOWED_KEYS = {
31
+ "epic": {
32
+ "id", "title", "status", "date_created", "date_started", "date_completed",
33
+ "dates_previously_completed", "reopened_on", "reopened_reason", "stories", "docs",
34
+ },
35
+ "story": {
36
+ "id", "epic_id", "title", "status", "date_created", "date_started", "date_completed",
37
+ "dates_previously_completed", "reopened_on", "reopened_reason", "tasks", "docs",
38
+ },
39
+ "task": {
40
+ "id", "story_id", "epic_id", "title", "status", "date_created", "date_started", "date_completed",
41
+ "dates_previously_completed", "reopened_on", "reopened_reason", "assigned_to", "docs",
42
+ },
43
+ }
44
+
45
+ ID_PATTERNS = {
46
+ "epic": re.compile(r"^E\d{2}$"),
47
+ "story": re.compile(r"^E\d{2}_S\d{2}$"),
48
+ "task": re.compile(r"^E\d{2}_S\d{2}_T\d{2}$"),
49
+ }
50
+
51
+ ASSIGNEES = {"developer", "tester", "scrum-master"}
52
+
53
+ def split_comment(value: str) -> str:
54
+ if "#" in value:
55
+ return value.split("#", 1)[0].rstrip()
56
+ return value.rstrip()
57
+
58
+ def extract_frontmatter(text: str, source: Path) -> list[str]:
59
+ lines = text.splitlines()
60
+ if not lines or lines[0].strip() != "---":
61
+ raise ValueError(f"{source}: missing opening frontmatter delimiter")
62
+ try:
63
+ end_index = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
64
+ except StopIteration:
65
+ raise ValueError(f"{source}: missing closing frontmatter delimiter")
66
+ return lines[1:end_index]
67
+
68
+ def classify_item(item_id: str) -> str:
69
+ for item_type, pattern in ID_PATTERNS.items():
70
+ if pattern.match(item_id):
71
+ return item_type
72
+ raise ValueError(f"unrecognised board item id format: {item_id}")
73
+
74
+ def parse_frontmatter(lines: list[str], source: Path) -> dict:
75
+ data = {}
76
+ i = 0
77
+ while i < len(lines):
78
+ raw = lines[i]
79
+ if not raw.strip():
80
+ i += 1
81
+ continue
82
+ if raw.startswith(" - "):
83
+ raise ValueError(f"{source}: list item without a parent key: {raw.strip()}")
84
+ if ":" not in raw:
85
+ raise ValueError(f"{source}: invalid frontmatter line: {raw.strip()}")
86
+
87
+ key, value = raw.split(":", 1)
88
+ key = key.strip()
89
+ value = split_comment(value.strip())
90
+
91
+ if value == "":
92
+ items = []
93
+ j = i + 1
94
+ while j < len(lines) and lines[j].startswith(" - "):
95
+ items.append(split_comment(lines[j][4:].strip()))
96
+ j += 1
97
+ data[key] = items if items else ""
98
+ i = j
99
+ continue
100
+
101
+ if value.startswith("[") and value.endswith("]"):
102
+ inner = value[1:-1].strip()
103
+ if inner == "":
104
+ data[key] = []
105
+ else:
106
+ parts = [part.strip() for part in inner.split(",")]
107
+ parsed = []
108
+ for part in parts:
109
+ parsed.append(part.strip().strip('"').strip("'"))
110
+ data[key] = parsed
111
+ else:
112
+ data[key] = value.strip().strip('"').strip("'")
113
+ i += 1
114
+ return data
115
+
116
+ def validate_docs(source: Path, docs):
117
+ if docs == "":
118
+ raise ValueError(f"{source}: docs must be a YAML list when present")
119
+ if not isinstance(docs, list):
120
+ raise ValueError(f"{source}: docs must be a YAML list")
121
+ for entry in docs:
122
+ if not isinstance(entry, str) or not entry.strip():
123
+ raise ValueError(f"{source}: docs entries must be non-empty strings")
124
+ cleaned = entry.strip()
125
+ if (
126
+ cleaned.startswith("/")
127
+ or cleaned.startswith("./")
128
+ or cleaned.startswith("../")
129
+ or "/./" in cleaned
130
+ or cleaned.endswith("/.")
131
+ or "/../" in cleaned
132
+ or cleaned == ".."
133
+ ):
134
+ raise ValueError(f"{source}: docs entry must be repo-relative without leading './' or '/' ({entry})")
135
+
136
+ def validate_file(source: Path):
137
+ text = source.read_text(encoding="utf-8")
138
+ frontmatter_lines = extract_frontmatter(text, source)
139
+ data = parse_frontmatter(frontmatter_lines, source)
140
+
141
+ item_id = data.get("id")
142
+ if not item_id:
143
+ raise ValueError(f"{source}: missing required 'id' frontmatter field")
144
+
145
+ item_type = classify_item(item_id)
146
+ allowed_keys = ALLOWED_KEYS[item_type]
147
+ unknown_keys = sorted(set(data.keys()) - allowed_keys)
148
+ if unknown_keys:
149
+ raise ValueError(f"{source}: unknown frontmatter field(s) for {item_type}: {', '.join(unknown_keys)}")
150
+
151
+ status = data.get("status")
152
+ if status and status not in STATUS_VALUES:
153
+ raise ValueError(f"{source}: invalid status '{status}'")
154
+
155
+ docs = data.get("docs", None)
156
+ if docs is not None:
157
+ validate_docs(source, docs)
158
+
159
+ if item_type == "epic":
160
+ if "stories" in data and data["stories"] != "" and not isinstance(data["stories"], list):
161
+ raise ValueError(f"{source}: stories must be a YAML list when present")
162
+ elif item_type == "story":
163
+ if not data.get("epic_id"):
164
+ raise ValueError(f"{source}: story files require epic_id")
165
+ if "tasks" in data and data["tasks"] != "" and not isinstance(data["tasks"], list):
166
+ raise ValueError(f"{source}: tasks must be a YAML list when present")
167
+ elif item_type == "task":
168
+ if not data.get("epic_id") or not data.get("story_id"):
169
+ raise ValueError(f"{source}: task files require both epic_id and story_id")
170
+ assigned_to = data.get("assigned_to", "")
171
+ if assigned_to and assigned_to not in ASSIGNEES:
172
+ raise ValueError(f"{source}: assigned_to must be one of: developer, tester, scrum-master")
173
+
174
+ print(f"✅ {source}: board frontmatter valid ({item_type})")
175
+
176
+ exit_code = 0
177
+ for raw_path in sys.argv[1:]:
178
+ path = Path(raw_path)
179
+ if not path.is_file():
180
+ print(f"❌ {path}: file not found or not readable", file=sys.stderr)
181
+ exit_code = 1
182
+ continue
183
+ try:
184
+ validate_file(path)
185
+ except Exception as exc:
186
+ print(f"❌ {exc}", file=sys.stderr)
187
+ exit_code = 1
188
+
189
+ sys.exit(exit_code)
190
+ PY
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # validate-story-format.sh — validate a story markdown file against required format rules
3
+ #
4
+ # Usage: ./scripts/validate-story-format.sh <path-to-story-file>
5
+ #
6
+ # Exit codes:
7
+ # 0 Story format is valid
8
+ # 1 File not found or not readable
9
+ # 2 Missing "## Acceptance Criteria" section
10
+ # 3 Missing "## Definition of Done" section
11
+ # 4 Definition of Done has no checkbox items
12
+
13
+ set -euo pipefail
14
+
15
+ FILE="${1:-}"
16
+
17
+ if [[ -z "$FILE" ]]; then
18
+ echo "❌ Usage: $(basename "$0") <path-to-story-file>"
19
+ exit 1
20
+ fi
21
+
22
+ if [[ ! -f "$FILE" ]] || [[ ! -r "$FILE" ]]; then
23
+ echo "❌ File not found or not readable: $FILE"
24
+ exit 1
25
+ fi
26
+
27
+ STORY_ID=$(basename "$FILE" .md)
28
+
29
+ # Check 2: ## Acceptance Criteria section present
30
+ if ! grep -q "^## Acceptance Criteria" "$FILE"; then
31
+ echo "❌ $STORY_ID: missing '## Acceptance Criteria' section — $FILE"
32
+ exit 2
33
+ fi
34
+
35
+ # Check 3: ## Definition of Done section present
36
+ if ! grep -q "^## Definition of Done" "$FILE"; then
37
+ echo "❌ $STORY_ID: missing '## Definition of Done' section — $FILE"
38
+ exit 3
39
+ fi
40
+
41
+ # Check 4: DoD section contains at least one checkbox item.
42
+ # Allow unchecked boxes in planned stories and checked boxes after tester verification.
43
+ # Extract content of the DoD section (lines after ## Definition of Done up to the next ## heading or EOF)
44
+ DOD_CONTENT=$(awk '/^## Definition of Done/{found=1; next} found && /^## /{found=0} found{print}' "$FILE")
45
+
46
+ if ! echo "$DOD_CONTENT" | grep -Eq "^- \[( |x)\]"; then
47
+ echo "❌ $STORY_ID: '## Definition of Done' has no checkbox items — $FILE"
48
+ echo " Each DoD item must use '- [ ] <criterion>' or '- [x] <criterion>', not plain bullets"
49
+ exit 4
50
+ fi
51
+
52
+ echo "✅ $STORY_ID: story format valid"
53
+ exit 0
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: brainstorm
3
+ description: Engage the scrum-master agent in a focused planning session to define, refine, or challenge features, improvements, tasks, stories, and epics. The agent asks probing questions, challenges assumptions, and helps shape ideas into actionable backlog items.
4
+ keywords:
5
+ - brainstorm
6
+ - plan
7
+ - ideate
8
+ - feature planning
9
+ - requirements
10
+ examples:
11
+ - "let's brainstorm ideas for X"
12
+ - "I need to plan a new feature"
13
+ metadata:
14
+ prefered_agent: scrum-master
15
+ ---
16
+
17
+ # Brainstorm — Collaborative Planning with the Scrum Master
18
+
19
+ ## Instructions
20
+
21
+ 1. **Invoke the `scrum-master` agent** — Pass the user's input directly to it as the opening prompt for the brainstorm session.
22
+
23
+ 2. **Session framing** — Tell the `scrum-master` agent:
24
+ - This is a **brainstorm session**, not a direct backlog-write session
25
+ - The goal is to explore, challenge, and refine ideas **before** committing anything to the board
26
+ - The agent should be especially frank, inquisitive, and suggestive during this session
27
+ - No board items should be created until the user explicitly says they are ready to commit
28
+
29
+ 3. **The `scrum-master` agent should:**
30
+ - Ask focused, pointed questions to uncover goals, constraints, edge cases, and unknowns
31
+ - Actively challenge assumptions — if something sounds vague, under-scoped, over-scoped, or contradicts existing work, say so plainly
32
+ - Suggest alternative framings, decompositions, or approaches when the current one seems weak
33
+ - Propose how the idea maps to epics, stories, or tasks — but hold off on writing anything until agreed
34
+ - Keep the dialogue moving: after each exchange, either surface the next open question or propose a next step
35
+
36
+ 4. **End of brainstorm** — Once the user is satisfied with the shape of the work, ask:
37
+ - "Are you ready to commit these items to the board?"
38
+ - If **yes** — proceed to create the relevant board items using `/todo` or the appropriate scrum board commands
39
+ - If **no** — continue refining or close the session
40
+
41
+ ### Session End
42
+
43
+ When the brainstorming session concludes (whether items were committed or the user chose to close), emit the following signal on its own line so the Jenga Router clears the active session:
44
+
45
+ ```
46
+ [JENGA:SESSION_END:brainstorm]
47
+ ```
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: btw
3
+ description: Capture a new mission (feature, change, or addition) and fit it into the project's Epic/Story structure, then choose to implement now or defer.
4
+ keywords:
5
+ - btw
6
+ - capture
7
+ - mission
8
+ - new idea
9
+ - side task
10
+ examples:
11
+ - "btw I also need to add X"
12
+ - "capture this as a new task"
13
+ metadata:
14
+ prefered_agent: scrum-master
15
+ ---
16
+
17
+ # BTW — Capture a Mission
18
+
19
+ ## Instructions
20
+
21
+ 1. **Gather mission details** — Ask the user:
22
+ - Where do you want to do this?
23
+ - What would you like to do?
24
+ - What is the goal?
25
+
26
+ 2. **Classify the mission** — Based on the description, determine if it fits into:
27
+ - An existing story
28
+ - A new story inside an existing Epic
29
+ - A new story inside a new Epic
30
+
31
+ 3. **Update project documentation** — Add the mission to the appropriate board files:
32
+ - **New story**: create a file in `project/board/stories/` using `../pi-plan/assets/story_template.md` as the structure.
33
+ - **New epic**: append a new Epic object to `project/PROJECT_SUMMARY.md` using the schema in `../pi-plan/assets/epic.json`.
34
+ - **Existing story**: append the new task or acceptance criterion to the matching file in `project/board/stories/`.
35
+
36
+ 4. **Ask the user: "now" or "later"?**
37
+ - If **"now"** — Produce a plan for the first story related to the mission and begin implementation.
38
+ - If **"later"** — Confirm the mission has been recorded and resume the previous workflow.
39
+
40
+ ## Examples
41
+ - `/btw` — Start the interactive mission capture flow
42
+ - `/btw add a dark mode toggle to the settings page` — Pre-fill the "what" and jump into classification