@jenga-ai/agent 1.0.1 → 1.1.1

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 (117) hide show
  1. package/README.md +10 -7
  2. package/agents/developer.md +82 -2
  3. package/agents/scrum-master.md +215 -21
  4. package/agents/tester.md +90 -8
  5. package/hooks/on_session_end.sh +171 -20
  6. package/mcp/router/embedder.js +1 -1
  7. package/mcp/training_runner/index.js +239 -0
  8. package/mcp/training_runner/package-lock.json +1065 -0
  9. package/mcp/training_runner/package.json +15 -0
  10. package/package.json +14 -16
  11. package/scripts/check-permission-level.sh +107 -0
  12. package/scripts/check-publicignore-match.sh +122 -0
  13. package/scripts/check-worktree-liveness.sh +193 -0
  14. package/scripts/generate-rapport-manifest.sh +43 -0
  15. package/scripts/idea_manager.sh +47 -0
  16. package/scripts/install-worktree-commit-guard.sh +134 -0
  17. package/scripts/jenga-permission-level-switch.sh +109 -0
  18. package/scripts/smoke-harness.sh +139 -0
  19. package/scripts/validate-board.sh +62 -0
  20. package/scripts/with-lock.sh +158 -0
  21. package/scripts/worktree-remove-guard.sh +204 -0
  22. package/skills/clearify/SKILL.md +52 -0
  23. package/skills/close-story/SKILL.md +203 -0
  24. package/skills/close-story/scripts/check-story-closeable.sh +195 -0
  25. package/skills/close-story/scripts/compute-scope-divergence.sh +128 -0
  26. package/skills/close-story/scripts/extract-diff-stats.sh +48 -0
  27. package/skills/close-story/scripts/extract-task-diff-stats.sh +97 -0
  28. package/skills/close-story/scripts/update-task-frontmatter.sh +103 -0
  29. package/skills/commit/SKILL.md +30 -3
  30. package/skills/distribute/CONFIG_SCHEMA.md +148 -0
  31. package/skills/distribute/SKILL.md +173 -0
  32. package/skills/distribute/scripts/check-version.sh +74 -0
  33. package/skills/distribute/scripts/commit-version-bump.sh +108 -0
  34. package/skills/distribute/scripts/distribute-changes.sh +381 -0
  35. package/skills/do/SKILL.md +352 -1
  36. package/skills/do/assets/intent-vs-diff-prompt.md +69 -0
  37. package/skills/doc/assets/path-objectives.yaml +13 -0
  38. package/skills/doc-sync/SKILL.md +16 -0
  39. package/skills/doc-sync/assets/doc_targets.md +11 -0
  40. package/skills/idea/SKILL.md +56 -0
  41. package/skills/idea/assets/idea_handoff_template.md +26 -0
  42. package/skills/idea/assets/idea_template.md +3 -0
  43. package/skills/init/SKILL.md +101 -7
  44. package/skills/init/assets/directory_structure.txt +1 -0
  45. package/skills/init/assets/strategy_stub_template.md +38 -0
  46. package/skills/init/assets/workflow_template.json +1 -1
  47. package/skills/init/scripts/apply-project-visibility.sh +176 -0
  48. package/skills/init/scripts/detect-existing-codebase.sh +166 -0
  49. package/skills/init/scripts/init.sh +35 -1
  50. package/skills/jenga/SKILL.md +206 -14
  51. package/skills/jenga/scripts/board-scan.sh +238 -0
  52. package/skills/jenga/scripts/cascade-resolve.sh +297 -0
  53. package/skills/jenga/scripts/render-confirmation.sh +679 -0
  54. package/skills/jenga/scripts/render-picker.sh +439 -0
  55. package/skills/jenga/scripts/resolve-id.sh +367 -0
  56. package/skills/jenga-permission-level/SKILL.md +81 -0
  57. package/skills/proceed/SKILL.md +1 -1
  58. package/skills/publish/SKILL.md +8 -5
  59. package/skills/publish/assets/ci-contract.md +2 -2
  60. package/skills/publish/assets/ownership-matrix.md +1 -1
  61. package/skills/publish/scripts/finalize_changelog.sh +115 -0
  62. package/skills/publish/scripts/generate_release_notes.sh +475 -28
  63. package/skills/publish/scripts/npm_ci_pipeline.sh +44 -6
  64. package/skills/publish/scripts/publish_deploy.sh +38 -8
  65. package/skills/publish/scripts/run_gates.sh +2 -2
  66. package/skills/reconcile/SKILL.md +117 -5
  67. package/skills/reconcile/scripts/detect-unlinked-code.sh +741 -0
  68. package/skills/skillify/assets/init-new/assets/directory_structure.txt +5 -1
  69. package/skills/spinoff/SKILL.md +12 -7
  70. package/skills/todo/SKILL.md +2 -0
  71. package/skills/uncharted/SKILL.md +711 -0
  72. package/skills/uncharted/assets/SEGMENT_PROPOSAL_TEMPLATE.md +129 -0
  73. package/skills/uncharted/assets/UNDERSTANDING_DOC_TEMPLATE.md +160 -0
  74. package/skills/uncharted/scripts/apply-subsystem-cap.sh +573 -0
  75. package/skills/uncharted/scripts/detect-dependencies.sh +732 -0
  76. package/skills/uncharted/scripts/detect-tests.sh +553 -0
  77. package/skills/uncharted/scripts/discover-subsystems.sh +1029 -0
  78. package/skills/uncharted/scripts/enumerate-target.sh +470 -0
  79. package/skills/uncharted/scripts/import-source.sh +517 -0
  80. package/skills/uncharted/scripts/inspect-provenance.sh +573 -0
  81. package/skills/uncharted/scripts/resolve-segment-target.sh +640 -0
  82. package/skills/uncharted/scripts/run-engine.sh +655 -0
  83. package/skills/uncharted/scripts/validate-proposed-items.sh +125 -0
  84. package/skills/uncharted/scripts/write-backfilled-epics.sh +498 -0
  85. package/skills/wtf/SKILL.md +20 -0
  86. package/templates/CHANGELOG_TEMPLATE.md +13 -0
  87. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +4 -1
  88. package/templates/SCRUM_BOARD_SCHEMA.md +206 -10
  89. package/templates/permission-levels/README.md +73 -0
  90. package/templates/permission-levels/level-1-locked.json +71 -0
  91. package/templates/permission-levels/level-2-guarded.json +64 -0
  92. package/templates/permission-levels/level-3-standard.json +62 -0
  93. package/templates/permission-levels/level-4-elevated.json +60 -0
  94. package/templates/permission-levels/level-5-unrestricted.json +58 -0
  95. package/skills/convert/SKILL.md +0 -124
  96. package/skills/convert/convert_cli.py +0 -235
  97. package/skills/convert/tests/sample.csv +0 -4
  98. package/skills/convert/tests/sample.json +0 -5
  99. package/skills/convert/tests/sample.jsonl +0 -3
  100. package/skills/convert/tests/sample.yaml +0 -18
  101. package/skills/convert/tests/sample_obj.csv +0 -2
  102. package/skills/convert/tests/sample_obj.json +0 -9
  103. package/skills/mirror-public/SKILL.md +0 -237
  104. package/skills/mirror-public/assets/config.json +0 -5
  105. package/skills/mirror-public/scripts/mirror.sh +0 -374
  106. package/skills/self-sync/SKILL.md +0 -73
  107. package/skills/self-sync/scripts/run.js +0 -136
  108. package/skills/train/SKILL.md +0 -116
  109. package/skills/train/assets/dashboard-templates/classifiers.html +0 -106
  110. package/skills/train/assets/dashboard-templates/nlp.html +0 -102
  111. package/skills/train/assets/dashboard-templates/transformers.html +0 -98
  112. package/skills/train/assets/results-parsers/__init__.py +0 -9
  113. package/skills/train/assets/results-parsers/classifiers.py +0 -84
  114. package/skills/train/assets/results-parsers/nlp.py +0 -88
  115. package/skills/train/assets/results-parsers/reporter.py +0 -154
  116. package/skills/train/assets/results-parsers/transformers.py +0 -120
  117. package/skills/train/train_cli.py +0 -786
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "training-runner-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that validates and runs ML training jobs via bash start.sh",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "start": "node index.js"
9
+ },
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.10.1",
12
+ "js-yaml": "^4.1.0",
13
+ "zod": "^3.23.8"
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,17 +1,13 @@
1
1
  {
2
2
  "name": "@jenga-ai/agent",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "JengaAgent — agentic project management CLI",
5
5
  "type": "module",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/samwelmunga/jenga-npm.git"
9
- },
10
6
  "publishConfig": {
11
7
  "access": "public"
12
8
  },
13
9
  "bin": {
14
- "jenga": "bin/jenga.js"
10
+ "jenga": "./bin/jenga.js"
15
11
  },
16
12
  "scripts": {
17
13
  "postinstall": "node scripts/postinstall.js",
@@ -30,22 +26,24 @@
30
26
  "templates/",
31
27
  "bin/",
32
28
  "lib/",
33
- "mcp/execute-ticket/",
34
- "mcp/help/",
35
- "mcp/router/index.js",
36
- "mcp/router/matcher.js",
37
- "mcp/router/embedder.js",
38
- "mcp/router/skill-index.js",
39
- "mcp/router/package.json",
40
- "mcp/router/package-lock.json",
41
- "mcp/router/README.md",
29
+ "mcp/",
42
30
  "README.md",
43
31
  "LICENSE"
44
32
  ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/samwelmunga/jenga-npm"
36
+ },
45
37
  "keywords": [],
46
38
  "author": "",
47
39
  "license": "MIT",
48
40
  "dependencies": {
49
- "@xenova/transformers": "^2.17.2"
41
+ "@huggingface/transformers": "^4.2.0"
42
+ },
43
+ "overrides": {
44
+ "sharp": "^0.34.5"
45
+ },
46
+ "comments": {
47
+ "audit": "sharp <0.35.0 and adm-zip <0.6.0 are transitive deps of @huggingface/transformers with no upstream fix available. Not exploitable in this context: only text feature-extraction pipeline is used — no image processing or ZIP handling at application boundary. Review when @huggingface/transformers ships a patched release."
50
48
  }
51
49
  }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # check-permission-level.sh — read-only gate for a skill's declared minimum
4
+ # permission level (the `minimum_permission_level` SKILL.md frontmatter field,
5
+ # see docs/skill-authoring.md).
6
+ #
7
+ # ============================================================================
8
+ # CHECK vs ENFORCE — READ THIS BEFORE WIRING THIS SCRIPT INTO A SKILL
9
+ # ============================================================================
10
+ # This script is READ-ONLY. It CHECKS the current session's permission level
11
+ # against a required minimum and SIGNALS the result. It does NOT:
12
+ # - prompt the user for confirmation
13
+ # - elevate the session level
14
+ # - write to .jenga-permission-level.json, .claude/settings.json, or
15
+ # .agents/settings.json under any code path
16
+ #
17
+ # The CALLING SKILL's own instructions are responsible for:
18
+ # 1. Detecting the `NEEDS_CONFIRMATION` signal (non-zero exit) below.
19
+ # 2. Explicitly asking the user to confirm elevation (no silent
20
+ # auto-elevation, ever).
21
+ # 3. On confirmation, invoking `jenga-permission-level-switch.sh <minimum-level>`
22
+ # (a sibling script, scripts/jenga-permission-level-switch.sh — task E33_S02_T02) to
23
+ # actually perform the elevation.
24
+ # 4. Immediately after the skill's own work completes, resetting the
25
+ # session back to Guarded (2) — typically via `jenga-permission-level-switch.sh 2` as
26
+ # the skill's last step. This reset is NOT automatic and is NOT this
27
+ # script's job.
28
+ #
29
+ # Usage:
30
+ # scripts/check-permission-level.sh <minimum-level>
31
+ #
32
+ # <minimum-level> Integer 1-5 — the value a calling skill read from its
33
+ # own `minimum_permission_level` frontmatter field.
34
+ #
35
+ # Exit codes:
36
+ # 0 Current session level already meets <minimum-level>. No output.
37
+ # 1 Current session level is below <minimum-level>. Prints:
38
+ # NEEDS_CONFIRMATION: elevate from <current> to <minimum-level>
39
+ # 2 Bad usage / invalid argument. Prints an error to stderr.
40
+ #
41
+ # Current level source:
42
+ # .jenga-permission-level.json at the repo root, field `session_level`
43
+ # (the format `jenga-permission-level-switch.sh`, E33_S02_T02, writes: {"session_level": <n>}).
44
+ # Missing file, unreadable file, malformed JSON, or a missing/non-integer
45
+ # `session_level` field are all treated as level 2 (Guarded), the
46
+ # permanent default — this script never fails merely because the level
47
+ # file is absent or malformed.
48
+ # ============================================================================
49
+
50
+ set -euo pipefail
51
+
52
+ usage() {
53
+ echo "Usage: $(basename "$0") <minimum-level>" >&2
54
+ echo " <minimum-level> must be an integer 1-5 (the skill's minimum_permission_level)." >&2
55
+ }
56
+
57
+ if [ "$#" -ne 1 ]; then
58
+ usage
59
+ exit 2
60
+ fi
61
+
62
+ MIN_LEVEL="$1"
63
+
64
+ case "$MIN_LEVEL" in
65
+ 1|2|3|4|5) ;;
66
+ *)
67
+ echo "Error: <minimum-level> must be an integer 1-5, got: '$MIN_LEVEL'" >&2
68
+ usage
69
+ exit 2
70
+ ;;
71
+ esac
72
+
73
+ LEVEL_FILE=".jenga-permission-level.json"
74
+ CURRENT_LEVEL=2
75
+
76
+ if [ -f "$LEVEL_FILE" ]; then
77
+ if command -v python3 >/dev/null 2>&1; then
78
+ PARSED=$(python3 -c "
79
+ import json, sys
80
+ try:
81
+ with open('$LEVEL_FILE') as f:
82
+ data = json.load(f)
83
+ level = data.get('session_level')
84
+ if isinstance(level, bool) or not isinstance(level, int) or not (1 <= level <= 5):
85
+ raise ValueError('invalid session_level')
86
+ print(level)
87
+ except Exception:
88
+ print(2)
89
+ " 2>/dev/null || echo 2)
90
+ CURRENT_LEVEL="$PARSED"
91
+ else
92
+ # No python3 available — fall back to a permissive grep-based extraction.
93
+ # Any failure to confidently parse an integer 1-5 falls back to level 2.
94
+ GREPPED=$(grep -o '"session_level"[[:space:]]*:[[:space:]]*[0-9]' "$LEVEL_FILE" 2>/dev/null | grep -o '[0-9]$' | head -n1 || true)
95
+ case "$GREPPED" in
96
+ 1|2|3|4|5) CURRENT_LEVEL="$GREPPED" ;;
97
+ *) CURRENT_LEVEL=2 ;;
98
+ esac
99
+ fi
100
+ fi
101
+
102
+ if [ "$CURRENT_LEVEL" -ge "$MIN_LEVEL" ]; then
103
+ exit 0
104
+ fi
105
+
106
+ echo "NEEDS_CONFIRMATION: elevate from $CURRENT_LEVEL to $MIN_LEVEL"
107
+ exit 1
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ # ---------------------------------------------------------------------------
3
+ # scripts/check-publicignore-match.sh
4
+ #
5
+ # Classify one or more repo-relative paths as PUBLIC (would ship to the
6
+ # public mirror repo) or BLOCKED (excluded by .publicignore), using the
7
+ # exact same rsync --exclude-from=.publicignore matching semantics as
8
+ # skills/mirror-public/scripts/mirror.sh's --dry-run "ship list" computation
9
+ # (see the SHIP_LIST_FILE block in that script). A file classified as
10
+ # "would be blocked" by `/mirror-public --dry-run` is guaranteed to be
11
+ # classified as BLOCKED here too, and vice versa for PUBLIC.
12
+ #
13
+ # This does NOT touch the network, clone the public repo, or require
14
+ # /mirror-public to be configured (skills/mirror-public/assets/config.json
15
+ # is never read) — it only needs a .publicignore file at the repo root.
16
+ # Rsync's include/exclude filter evaluation does not depend on destination
17
+ # state (destination state only affects delete/itemize-flag details for
18
+ # files already present there), so probing against an empty scratch temp
19
+ # directory yields an identical "would ship" set to probing against
20
+ # mirror.sh's real public-clone destination.
21
+ #
22
+ # Callers (e.g. /doc-sync step 4, and the E36 changelog generator's
23
+ # .publicignore-awareness) are expected to skip invoking this script
24
+ # entirely when .publicignore does not exist at the repo root — that is
25
+ # the established no-op-when-absent precedent, and it stays owned by each
26
+ # caller rather than being silently absorbed here.
27
+ #
28
+ # Usage:
29
+ # check-publicignore-match.sh <path> [<path> ...]
30
+ #
31
+ # Paths are interpreted relative to the repo root (same convention as
32
+ # `git ls-files`). Output: one "STATUS<TAB>path" line per input path, in
33
+ # input order. STATUS is PUBLIC or BLOCKED.
34
+ #
35
+ # Note: a path that does not exist on disk is classified BLOCKED (it will
36
+ # not appear in rsync's transfer list regardless of .publicignore). Callers
37
+ # such as /doc-sync only ever pass paths already confirmed to exist as
38
+ # real new-in-source candidates, so this does not arise in practice — but
39
+ # it is not the same guarantee as ".publicignore explicitly matched it".
40
+ #
41
+ # Exit codes:
42
+ # 0 classification completed (regardless of individual PUBLIC/BLOCKED results)
43
+ # 1 usage error
44
+ # 2 repo root not found, .publicignore missing, or rsync unavailable
45
+ # ---------------------------------------------------------------------------
46
+
47
+ set -euo pipefail
48
+ IFS=$'\n\t'
49
+
50
+ die() {
51
+ printf 'check-publicignore-match.sh: error: %s\n' "$*" >&2
52
+ exit 2
53
+ }
54
+
55
+ if [ "$#" -eq 0 ]; then
56
+ printf 'usage: check-publicignore-match.sh <path> [<path> ...]\n' >&2
57
+ exit 1
58
+ fi
59
+
60
+ SCRIPT_PATH="${BASH_SOURCE[0]}"
61
+ while [ -h "$SCRIPT_PATH" ]; do
62
+ LINK_TARGET="$(readlink "$SCRIPT_PATH")"
63
+ case "$LINK_TARGET" in
64
+ /*) SCRIPT_PATH="$LINK_TARGET" ;;
65
+ *) SCRIPT_PATH="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)/$LINK_TARGET" ;;
66
+ esac
67
+ done
68
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
69
+
70
+ REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
71
+ [ -n "$REPO_ROOT" ] || die "could not locate repo root (git rev-parse failed from $SCRIPT_DIR)"
72
+
73
+ PUBLICIGNORE="$REPO_ROOT/.publicignore"
74
+ [ -f "$PUBLICIGNORE" ] || die "no .publicignore at $PUBLICIGNORE (caller should skip invoking this script when absent — that is the no-op case, not an error the caller should surface)"
75
+
76
+ command -v rsync >/dev/null 2>&1 || die "rsync not installed"
77
+
78
+ TMP_DEST="$(mktemp -d)"
79
+ SHIP_LIST_FILE="$(mktemp)"
80
+ cleanup() {
81
+ rm -rf "$TMP_DEST"
82
+ rm -f "$SHIP_LIST_FILE"
83
+ }
84
+ trap cleanup EXIT
85
+
86
+ # Same rsync flags + itemize/awk filter as mirror.sh's --dry-run ship-list
87
+ # computation (kept in lockstep with that block intentionally), targeted at
88
+ # an empty scratch dir instead of a real public clone.
89
+ rsync -a --delete \
90
+ --exclude-from="$PUBLICIGNORE" \
91
+ --exclude=".git" \
92
+ --dry-run --itemize-changes --out-format='%i %n' \
93
+ "$REPO_ROOT/" \
94
+ "$TMP_DEST/" \
95
+ | awk '{
96
+ flag = $1;
97
+ first = substr(flag, 1, 1);
98
+ second = substr(flag, 2, 1);
99
+ # Skip directories — public tree recreates them implicitly.
100
+ if (second == "d") next;
101
+ # Keep only entries that transfer or create a file/symlink/hardlink.
102
+ keep = 0;
103
+ if (first == ">" || first == "<") keep = 1; # file transfer
104
+ else if (first == "c" && second == "L") keep = 1; # create symlink
105
+ else if (first == "h") keep = 1; # hard link
106
+ if (!keep) next;
107
+ $1 = "";
108
+ sub(/^ /, "");
109
+ sub(/\/$/, "");
110
+ if (length($0) > 0) print $0;
111
+ }' \
112
+ | LC_ALL=C sort -u > "$SHIP_LIST_FILE"
113
+
114
+ for candidate in "$@"; do
115
+ # Normalize a single leading "./" if present, so callers can pass either form.
116
+ norm="${candidate#./}"
117
+ if LC_ALL=C grep -Fxq "$norm" "$SHIP_LIST_FILE"; then
118
+ printf 'PUBLIC\t%s\n' "$candidate"
119
+ else
120
+ printf 'BLOCKED\t%s\n' "$candidate"
121
+ fi
122
+ done
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/check-worktree-liveness.sh — detect OS processes still rooted
3
+ # inside a given worktree path.
4
+ #
5
+ # Usage:
6
+ # scripts/check-worktree-liveness.sh <worktree-path>
7
+ #
8
+ # Why this exists: `ListAgents` reports agent lifecycle, not detached OS
9
+ # processes. A live incident (2026-08-25, see E37's Purpose) showed a
10
+ # completed agent leaving shell polling loops running with their cwd inside
11
+ # a worktree; `ListAgents` showed the agent itself as "completed" and gave
12
+ # no signal that those loops were still alive, so the worktree was removed
13
+ # out from under them. This script answers a narrower, deterministic
14
+ # question instead: "does any OS process currently have its *current
15
+ # working directory* rooted inside this path (the path itself, or any
16
+ # subdirectory of it)?" It is intended to be run as a precondition before
17
+ # `git worktree remove` (wired in the sibling task E37_S03_T02, not here —
18
+ # this task only produces the standalone check).
19
+ #
20
+ # Detection mechanism (primary): `lsof +D <path> -F pcfn`
21
+ # `+D <path>` recursively restricts lsof's report to files open within
22
+ # that directory subtree (this alone gives us "inside the given path,
23
+ # including subdirectories" — verified locally against a nested-subdir
24
+ # case). `-F pcfn` is lsof's machine-parsable output mode, emitting one
25
+ # attribute per line: `p<pid>`, `c<command>`, `f<fd-or-descriptor-type>`,
26
+ # `n<file name>`, grouped per matching open-file record. Records are then
27
+ # filtered down to those whose `f` field is literally `cwd` — this is what
28
+ # distinguishes "this process's current working directory is here" from
29
+ # "this process merely has some unrelated file open under here" (e.g. a
30
+ # log file), which `+D` alone does not distinguish.
31
+ #
32
+ # `-F pcfn` (not lsof's default human-readable columnar output) is used
33
+ # deliberately: column widths/ordering in the default output are not
34
+ # guaranteed stable across lsof versions/platforms, whereas the `-F` field
35
+ # protocol is. Verified directly against this repo's own Darwin runtime
36
+ # (lsof 4.91) — both an exact-path cwd match and a subdirectory cwd match
37
+ # are correctly reported, and process cwd is resolved through symlinks
38
+ # (macOS reports `/tmp/x` as `/private/tmp/x`), which is why the input
39
+ # path is canonicalized below before comparison.
40
+ #
41
+ # Detection mechanism (fallback, when `lsof` is not on PATH): enumerate
42
+ # `/proc/[0-9]*` (Linux only — macOS has no `/proc`) and `readlink` each
43
+ # `/proc/<pid>/cwd`, comparing against the canonicalized target path. macOS
44
+ # ships `lsof` by default, so this branch is not expected to run on this
45
+ # repo's actual runtime, but keeps the script functional on minimal Linux
46
+ # containers that may lack `lsof`. If neither mechanism is available, the
47
+ # script cannot determine liveness at all and fails closed (non-zero exit,
48
+ # clear stderr message) rather than silently reporting "clear" when it does
49
+ # not actually know.
50
+ #
51
+ # Command-line resolution for reported matches uses `ps -o command= -p
52
+ # <pid>` — confirmed to behave identically on macOS's BSD `ps` and Linux's
53
+ # GNU `ps`, unlike some other `ps` flags/keywords (e.g. `-e` listing
54
+ # semantics differ between BSD and GNU `ps`, which is why the fallback path
55
+ # above enumerates PIDs via `/proc` rather than `ps -e`).
56
+ #
57
+ # Caller-cwd self-detection hardening: if the *caller* invokes this script
58
+ # from a shell whose own cwd is already inside the target path (e.g. `cd
59
+ # <worktree> && check-worktree-liveness.sh <worktree>` — a very plausible
60
+ # state right before a worktree is removed), every subprocess the script
61
+ # forks (the `lsof` child, and the process-substitution subshell wrapping
62
+ # it below, or the `readlink` calls in the /proc fallback) would otherwise
63
+ # *inherit* that cwd, since bash processes inherit cwd from their parent by
64
+ # default. That produced a real, reproduced false positive: the script's
65
+ # own subprocess chain got reported as a "live process rooted in" the very
66
+ # path being checked, even with zero genuinely orphaned processes running.
67
+ # See project/rapports/problems/E37_S03_T01-self-detection-false-positive-remarks.md
68
+ # for the original reproduction and the two fix options it proposed.
69
+ #
70
+ # Fix: immediately after resolving $ABS_PATH (and before spawning anything),
71
+ # the script `cd`s itself to `/` — a path guaranteed to be outside every
72
+ # possible target. This moves the *script's own process* outside the target,
73
+ # so every descendant it forks from this point on inherits a cwd of `/`
74
+ # instead of the caller's original cwd, regardless of where the caller
75
+ # happened to be standing when it invoked this script. This is preferred
76
+ # over tracking and excluding individual subprocess PIDs (the script's own
77
+ # PID `$$` was already excluded as a courtesy, see below) because `cd /`
78
+ # removes the precondition for the whole bug class structurally, rather
79
+ # than requiring every current and future subprocess this script might ever
80
+ # spawn to be enumerated and excluded by PID after the fact.
81
+ #
82
+ # The script also excludes its own PID ($$) from reported matches as a
83
+ # second, defense-in-depth courtesy (now largely redundant given the `cd /`
84
+ # fix above, but harmless to keep).
85
+ #
86
+ # Exit codes:
87
+ # 0 clear — no live process found, no output
88
+ # 1 one or more live processes found — PID(s) and command(s) printed
89
+ # 2 invalid usage (wrong argument count / empty argument)
90
+ # 3 <worktree-path> does not resolve to an existing directory
91
+ # 4 unable to determine liveness on this platform (neither `lsof` nor
92
+ # /proc available) — fail closed, this is NOT the same as "clear"
93
+
94
+ set -u
95
+
96
+ SCRIPT_NAME="check-worktree-liveness.sh"
97
+
98
+ usage() {
99
+ echo "Usage: $SCRIPT_NAME <worktree-path>" >&2
100
+ exit 2
101
+ }
102
+
103
+ [ "$#" -eq 1 ] || usage
104
+ RAW_PATH="$1"
105
+ [ -n "$RAW_PATH" ] || usage
106
+
107
+ if [ ! -d "$RAW_PATH" ]; then
108
+ echo "$SCRIPT_NAME: '$RAW_PATH' is not a directory" >&2
109
+ exit 3
110
+ fi
111
+
112
+ # Canonicalize to an absolute, symlink-resolved path so comparisons against
113
+ # process cwd values (which the OS reports fully resolved — e.g. macOS
114
+ # reports /tmp as /private/tmp) match correctly regardless of how the
115
+ # caller spelled the input path.
116
+ ABS_PATH="$(cd "$RAW_PATH" 2>/dev/null && pwd -P)"
117
+ if [ -z "$ABS_PATH" ]; then
118
+ echo "$SCRIPT_NAME: unable to resolve '$RAW_PATH' to an absolute path" >&2
119
+ exit 3
120
+ fi
121
+
122
+ # Move this process (and therefore every subprocess it forks from here on)
123
+ # outside of every possible target path, so the scan below can never
124
+ # self-detect regardless of the caller's cwd at invocation time. See the
125
+ # "Caller-cwd self-detection hardening" comment in the header above. `/` is
126
+ # always present and readable on every platform this script targets; if for
127
+ # some reason it could not be `cd`'d into, fall through rather than abort —
128
+ # worst case the pre-existing $$ exclusion below is the only mitigation,
129
+ # same as before this fix.
130
+ cd / 2>/dev/null || true
131
+
132
+ SELF_PID="$$"
133
+ FOUND_ANY=0
134
+ REPORTED_PIDS=""
135
+
136
+ already_reported() {
137
+ case " $REPORTED_PIDS " in
138
+ *" $1 "*) return 0 ;;
139
+ *) return 1 ;;
140
+ esac
141
+ }
142
+
143
+ report_match() {
144
+ pid="$1"
145
+ already_reported "$pid" && return 0
146
+ REPORTED_PIDS="$REPORTED_PIDS $pid"
147
+ cmd="$(ps -o command= -p "$pid" 2>/dev/null)"
148
+ [ -n "$cmd" ] || cmd="(command unavailable — process may have exited)"
149
+ echo "$SCRIPT_NAME: live process rooted in '$ABS_PATH' — PID $pid: $cmd"
150
+ FOUND_ANY=1
151
+ }
152
+
153
+ if command -v lsof >/dev/null 2>&1; then
154
+ current_pid=""
155
+ current_fd=""
156
+ while IFS= read -r line; do
157
+ case "$line" in
158
+ p*)
159
+ current_pid="${line#p}"
160
+ current_fd=""
161
+ ;;
162
+ f*)
163
+ current_fd="${line#f}"
164
+ ;;
165
+ n*)
166
+ if [ "$current_fd" = "cwd" ] && [ -n "$current_pid" ] && [ "$current_pid" != "$SELF_PID" ]; then
167
+ report_match "$current_pid"
168
+ fi
169
+ ;;
170
+ esac
171
+ done < <(lsof +D "$ABS_PATH" -F pcfn 2>/dev/null)
172
+ else
173
+ if [ -d /proc ]; then
174
+ for pid_dir in /proc/[0-9]*; do
175
+ [ -e "$pid_dir" ] || continue
176
+ pid="${pid_dir#/proc/}"
177
+ [ "$pid" != "$SELF_PID" ] || continue
178
+ cwd_link="$(readlink "$pid_dir/cwd" 2>/dev/null)" || continue
179
+ [ -n "$cwd_link" ] || continue
180
+ case "$cwd_link" in
181
+ "$ABS_PATH"|"$ABS_PATH"/*)
182
+ report_match "$pid"
183
+ ;;
184
+ esac
185
+ done
186
+ else
187
+ echo "$SCRIPT_NAME: neither 'lsof' nor /proc is available on this system — cannot determine worktree liveness for '$ABS_PATH'" >&2
188
+ exit 4
189
+ fi
190
+ fi
191
+
192
+ [ "$FOUND_ANY" -eq 1 ] && exit 1
193
+ exit 0
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # scripts/generate-rapport-manifest.sh
3
+ #
4
+ # Regenerates the rapport manifest used by hooks/on_session_end.sh to detect
5
+ # *new* problem rapports (project/rapports/problems/*.md, excluding resolved
6
+ # *.IGNORE.md files). This script is the single source of truth for "what
7
+ # does the manifest look like right now" — it is used both to produce the
8
+ # committed seed manifest and, at runtime, by the hook itself whenever it
9
+ # rewrites the manifest after finding new rapports.
10
+ #
11
+ # Entries are stored as paths relative to the repo root (e.g.
12
+ # "project/rapports/problems/foo.md"), NOT absolute paths. The manifest is
13
+ # a committed artifact shared across every clone/worktree, each of which
14
+ # has a different absolute checkout location — absolute paths would make
15
+ # the seed match nobody's filesystem but the one it was generated on.
16
+ #
17
+ # Safe to re-run at any time: it is a pure snapshot of the current directory
18
+ # contents, never a diff, so running it twice in a row produces identical
19
+ # output.
20
+ #
21
+ # Usage: scripts/generate-rapport-manifest.sh [output_file]
22
+ # output_file defaults to project/data/rapport_manifest.json
23
+
24
+ set -euo pipefail
25
+
26
+ # shellcheck source=lib/resolve-project-dir.sh
27
+ source "$(git rev-parse --show-toplevel)/lib/resolve-project-dir.sh"
28
+
29
+ PROJECT_DIR="$JENGA_PROJECT_DIR"
30
+ RAPPORT_DIR="$PROJECT_DIR/project/rapports/problems"
31
+ OUTPUT="${1:-$PROJECT_DIR/project/data/rapport_manifest.json}"
32
+
33
+ mkdir -p "$(dirname "$OUTPUT")"
34
+
35
+ if [ -d "$RAPPORT_DIR" ]; then
36
+ find "$RAPPORT_DIR" -name "*.md" ! -name "*.IGNORE.md" 2>/dev/null \
37
+ | sed "s|^$PROJECT_DIR/||" \
38
+ | sort | jq -R . | jq -s . > "$OUTPUT"
39
+ else
40
+ echo "[]" > "$OUTPUT"
41
+ fi
42
+
43
+ echo "Wrote $(jq 'length' "$OUTPUT") rapport filename(s) to $OUTPUT"
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/idea_manager.sh — canonical owner of all project/ideas.md operations.
3
+ # Run from the repository root.
4
+
5
+ IDEA_FILE="project/ideas.md"
6
+ TEMPLATE="skills/idea/assets/idea_template.md"
7
+
8
+ usage() {
9
+ cat >&2 <<EOF
10
+ Usage: $0 <subcommand> [args]
11
+
12
+ Subcommands:
13
+ add "<entry>" Append entry to project/ideas.md (auto-creates from template if missing)
14
+ list Print all non-comment, non-blank entries (silent if file missing/empty)
15
+ EOF
16
+ exit 1
17
+ }
18
+
19
+ # Filters out blank lines, lines starting with #, and HTML comments
20
+ real_entries() {
21
+ grep -v '^\s*$' "$1" 2>/dev/null \
22
+ | grep -v '^\s*#' \
23
+ | grep -v '^\s*<!--'
24
+ }
25
+
26
+ case "${1:-}" in
27
+ add)
28
+ [ -z "${2:-}" ] && { echo "Error: add requires an entry argument" >&2; exit 1; }
29
+ if [ ! -f "$IDEA_FILE" ]; then
30
+ if [ ! -f "$TEMPLATE" ]; then
31
+ echo "Error: template not found at $TEMPLATE" >&2; exit 1
32
+ fi
33
+ cp "$TEMPLATE" "$IDEA_FILE"
34
+ fi
35
+ printf '%s\n' "$2" >> "$IDEA_FILE"
36
+ ;;
37
+
38
+ list)
39
+ [ ! -f "$IDEA_FILE" ] && exit 0
40
+ real_entries "$IDEA_FILE"
41
+ exit 0
42
+ ;;
43
+
44
+ *)
45
+ usage
46
+ ;;
47
+ esac