@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.
- package/LICENSE +201 -0
- package/README.md +340 -0
- package/agents/ai_engineer.md +113 -0
- package/agents/developer.md +236 -0
- package/agents/scrum-master.md +349 -0
- package/agents/scrutiny-agent.md +137 -0
- package/agents/solution-assessor.md +185 -0
- package/agents/tester.md +339 -0
- package/bin/jenga.js +70 -0
- package/hooks/copilot_session_end.sh +29 -0
- package/hooks/on_session_end.sh +238 -0
- package/hooks/prompt_router.sh +11 -0
- package/hooks/prompt_router_helper.js +52 -0
- package/hooks/session_end_helper.js +29 -0
- package/hooks/session_end_watcher.sh +24 -0
- package/lib/commands/attach.js +47 -0
- package/lib/commands/init.js +207 -0
- package/lib/commands/start.js +16 -0
- package/lib/commands/status.js +53 -0
- package/lib/config-schema.js +72 -0
- package/lib/inject-settings.js +61 -0
- package/lib/mirror.js +244 -0
- package/lib/resolve-project-dir.sh +47 -0
- package/mcp/execute-ticket/index.js +10 -0
- package/mcp/execute-ticket/package.json +5 -0
- package/mcp/help/index.js +79 -0
- package/mcp/help/package.json +14 -0
- package/mcp/router/README.md +19 -0
- package/mcp/router/embedder.js +23 -0
- package/mcp/router/index.js +204 -0
- package/mcp/router/matcher.js +87 -0
- package/mcp/router/package-lock.json +1048 -0
- package/mcp/router/package.json +11 -0
- package/mcp/router/skill-index.js +104 -0
- package/package.json +47 -0
- package/scripts/board_resolver.sh +46 -0
- package/scripts/e25_s01_extract_board_graph.py +292 -0
- package/scripts/e25_s01_generate_synthetic_board.py +90 -0
- package/scripts/measurement-10x.json +50 -0
- package/scripts/measurement-10x.txt +4 -0
- package/scripts/measurement-real.json +50 -0
- package/scripts/measurement-real.txt +4 -0
- package/scripts/postinstall.js +165 -0
- package/scripts/todo_cleanup.sh +22 -0
- package/scripts/todo_manager.sh +86 -0
- package/scripts/validate-board.sh +190 -0
- package/scripts/validate-story-format.sh +53 -0
- package/skills/brainstorm/SKILL.md +47 -0
- package/skills/btw/SKILL.md +42 -0
- package/skills/commit/SKILL.md +29 -0
- package/skills/commit/assets/user_instructions_template.md +22 -0
- package/skills/continue/SKILL.md +29 -0
- package/skills/convert/SKILL.md +124 -0
- package/skills/convert/convert_cli.py +235 -0
- package/skills/convert/tests/sample.csv +4 -0
- package/skills/convert/tests/sample.json +5 -0
- package/skills/convert/tests/sample.jsonl +3 -0
- package/skills/convert/tests/sample.yaml +18 -0
- package/skills/convert/tests/sample_obj.csv +2 -0
- package/skills/convert/tests/sample_obj.json +9 -0
- package/skills/deep-dive/SKILL.md +167 -0
- package/skills/do/SKILL.md +88 -0
- package/skills/do/assets/sender_template.json +12 -0
- package/skills/doc/SKILL.md +314 -0
- package/skills/doc/assets/path-objectives.yaml +38 -0
- package/skills/doc-sync/SKILL.md +167 -0
- package/skills/doc-sync/assets/default_excludes.txt +21 -0
- package/skills/doc-sync/assets/doc_targets.md +14 -0
- package/skills/dooo/SKILL.md +60 -0
- package/skills/error/SKILL.md +29 -0
- package/skills/evaluate/SKILL.md +45 -0
- package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
- package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
- package/skills/examplify/SKILL.md +42 -0
- package/skills/help/SKILL.md +36 -0
- package/skills/improve/SKILL.md +55 -0
- package/skills/index/scripts/board-index +4 -0
- package/skills/index/scripts/board_index.py +615 -0
- package/skills/index/scripts/smoke_test.sh +86 -0
- package/skills/init/SKILL.md +44 -0
- package/skills/init/assets/.gitignore_template +15 -0
- package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/init/assets/directory_structure.txt +13 -0
- package/skills/init/assets/test-config_template.json +4 -0
- package/skills/init/assets/workflow_template.json +30 -0
- package/skills/init/scripts/init.sh +48 -0
- package/skills/jbp/SKILL.md +25 -0
- package/skills/jenga/SKILL.md +68 -0
- package/skills/lgtm/SKILL.md +21 -0
- package/skills/mirror-public/SKILL.md +237 -0
- package/skills/mirror-public/assets/config.json +5 -0
- package/skills/mirror-public/scripts/mirror.sh +374 -0
- package/skills/pi-plan/SKILL.md +62 -0
- package/skills/pi-plan/assets/epic.json +7 -0
- package/skills/pi-plan/assets/story_template.md +18 -0
- package/skills/proceed/SKILL.md +29 -0
- package/skills/publish/SKILL.md +351 -0
- package/skills/publish/adapters/droplet.md +200 -0
- package/skills/publish/adapters/mobile-ios.md +114 -0
- package/skills/publish/adapters/npm-ci.md +223 -0
- package/skills/publish/adapters/npm.md +121 -0
- package/skills/publish/assets/ExportOptions.plist.template +19 -0
- package/skills/publish/assets/ci-contract.md +111 -0
- package/skills/publish/assets/ownership-matrix.md +17 -0
- package/skills/publish/assets/publish.example.json +85 -0
- package/skills/publish/assets/publish.example.npm-ci.json +40 -0
- package/skills/publish/assets/publish.example.npm.json +41 -0
- package/skills/publish/assets/secrets-guide.md +104 -0
- package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
- package/skills/publish/schemas/publish.schema.json +428 -0
- package/skills/publish/scripts/check_target_config.sh +96 -0
- package/skills/publish/scripts/droplet_pipeline.sh +208 -0
- package/skills/publish/scripts/generate_release_notes.sh +200 -0
- package/skills/publish/scripts/ios_pipeline.sh +486 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
- package/skills/publish/scripts/npm_pipeline.sh +249 -0
- package/skills/publish/scripts/publish_common.sh +253 -0
- package/skills/publish/scripts/publish_deploy.sh +538 -0
- package/skills/publish/scripts/reconcile_tags.sh +135 -0
- package/skills/publish/scripts/run_gates.sh +616 -0
- package/skills/publish/scripts/setup_wizard.sh +394 -0
- package/skills/publish/scripts/show_history.sh +95 -0
- package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
- package/skills/publish/scripts/validate_config.sh +163 -0
- package/skills/publish/scripts/validate_droplet_env.sh +45 -0
- package/skills/publish/scripts/validate_ios_env.sh +68 -0
- package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
- package/skills/publish/scripts/validate_npm_env.sh +22 -0
- package/skills/publish/scripts/write_ledger_entry.sh +126 -0
- package/skills/publish/wizards/droplet.md +275 -0
- package/skills/publish/wizards/mobile-ios.md +157 -0
- package/skills/publish/wizards/npm-ci.md +240 -0
- package/skills/publish/wizards/npm.md +224 -0
- package/skills/reconcile/SKILL.md +93 -0
- package/skills/reconcile/assets/report_format.md +44 -0
- package/skills/reconcile-origin/SKILL.md +75 -0
- package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
- package/skills/redo/SKILL.md +70 -0
- package/skills/route/SKILL.md +180 -0
- package/skills/self-sync/SKILL.md +73 -0
- package/skills/self-sync/scripts/run.js +136 -0
- package/skills/skillify/SKILL.md +68 -0
- package/skills/skillify/assets/init-new/SKILL.md +35 -0
- package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
- package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
- package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
- package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
- package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
- package/skills/skillify/assets/init-old/SKILL.md +124 -0
- package/skills/spinoff/SKILL.md +48 -0
- package/skills/status/SKILL.md +33 -0
- package/skills/status/assets/output_format.md +41 -0
- package/skills/todo/SKILL.md +46 -0
- package/skills/todo/assets/todo_handoff_template.md +22 -0
- package/skills/todo/assets/todo_template.md +3 -0
- package/skills/train/SKILL.md +116 -0
- package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
- package/skills/train/assets/dashboard-templates/nlp.html +102 -0
- package/skills/train/assets/dashboard-templates/transformers.html +98 -0
- package/skills/train/assets/results-parsers/__init__.py +9 -0
- package/skills/train/assets/results-parsers/classifiers.py +84 -0
- package/skills/train/assets/results-parsers/nlp.py +88 -0
- package/skills/train/assets/results-parsers/reporter.py +154 -0
- package/skills/train/assets/results-parsers/transformers.py +120 -0
- package/skills/train/train_cli.py +786 -0
- package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
- package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
- package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
- package/templates/SKILL.md +16 -0
- package/templates/SKILL_TEMPLATE.md +28 -0
- package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
- 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,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
|