devflow-kit 3.1.0 → 3.3.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/CHANGELOG.md +52 -0
- package/README.md +2 -2
- package/dist/cli/agents-view/render.js +69 -15
- package/dist/cli/agents-view/state.js +40 -14
- package/dist/cli/commands/agents.js +135 -45
- package/dist/cli/commands/init.js +128 -53
- package/dist/cli/commands/learning.js +61 -13
- package/dist/cli/commands/memory.js +35 -14
- package/dist/cli/commands/uninstall.js +163 -39
- package/dist/commands/code-review.md +1 -3
- package/dist/commands/debug.md +15 -12
- package/dist/commands/dynamic-build.md +172 -135
- package/dist/commands/dynamic-plan.md +9 -3
- package/dist/commands/explore.md +10 -4
- package/dist/commands/implement.md +149 -145
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +8 -2
- package/dist/commands/research.md +8 -2
- package/dist/commands/resolve.md +28 -19
- package/dist/commands/self-review.md +16 -13
- package/dist/core/agent-frontmatter.js +25 -0
- package/dist/core/agent-models.js +201 -36
- package/dist/core/agent-state.js +27 -5
- package/dist/core/assets.js +1 -1
- package/dist/core/feature-config.js +68 -10
- package/dist/core/flags.js +24 -0
- package/dist/core/learning-queue-cleanup.js +10 -11
- package/dist/core/learning-tuning-config.js +8 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/plugins.js +16 -5
- package/dist/core/queue-drain.js +31 -0
- package/dist/hud/components/learning-counts.js +54 -8
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
- package/dist/targets/claude-code/installer.js +36 -9
- package/dist/targets/claude-code/post-install.js +128 -38
- package/package.json +1 -1
- package/src/assets/agents/code.md +85 -35
- package/src/assets/agents/design.md +12 -0
- package/src/assets/agents/diagnose.md +18 -11
- package/src/assets/agents/evaluate.md +17 -24
- package/src/assets/agents/knowledge.md +7 -3
- package/src/assets/agents/learning.md +4 -6
- package/src/assets/agents/research.md +21 -0
- package/src/assets/agents/review.md +12 -0
- package/src/assets/agents/scrutinize.md +37 -9
- package/src/assets/agents/simplify.md +24 -0
- package/src/assets/agents/skim.md +6 -2
- package/src/assets/agents/synthesize.md +18 -0
- package/src/assets/agents/test.md +19 -11
- package/src/assets/agents/triage.md +8 -0
- package/src/assets/agents/validate.md +20 -11
- package/src/assets/commands/_partials/_engine.mds +36 -55
- package/src/assets/commands/_partials/_knowledge.mds +1 -3
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/_partials/_wave.mds +8 -6
- package/src/assets/commands/code-review.mds +1 -3
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +126 -72
- package/src/assets/commands/dynamic-plan.mds +7 -1
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +147 -141
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +8 -2
- package/src/assets/commands/research.mds +8 -2
- package/src/assets/commands/resolve.mds +27 -16
- package/src/assets/commands/self-review.mds +15 -10
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +2 -2
- package/src/assets/mds/tracker/_jira.mds +2 -2
- package/src/assets/mds/tracker/_linear.mds +2 -2
- package/src/assets/scripts/ci-wait.cjs +636 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +356 -17
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +4 -3
- package/src/assets/scripts/hooks/ensure-devflow-init +13 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +122 -10
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/json-helper.cjs +12 -145
- package/src/assets/scripts/hooks/json-parse +24 -129
- package/src/assets/scripts/hooks/lib/learning-store.cjs +169 -64
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +53 -21
- package/src/assets/scripts/hooks/session-start-context +108 -29
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +9 -12
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +15 -33
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
|
@@ -58,6 +58,22 @@
|
|
|
58
58
|
# `!.claudeignore` entry means "the user owns that line" — emit the block without it.
|
|
59
59
|
# All matching is whole-line and whitespace-tolerant; never substring.
|
|
60
60
|
#
|
|
61
|
+
# D-GITIGNORE-LINK-INSIDE: a root .gitignore that is a symbolic link is written only
|
|
62
|
+
# when the file it resolves to lies inside the project root and outside any .git
|
|
63
|
+
# folder in the project, the project's own or a nested repository's: no part of its
|
|
64
|
+
# path below the root may be named .git, in any letter case, as git itself refuses
|
|
65
|
+
# such a path. Then that file is read and written directly, never through the link.
|
|
66
|
+
# Otherwise nothing is read or written anywhere, no marker is stamped, the skip is
|
|
67
|
+
# logged once and the caller goes on. Reason: a repository can commit .gitignore as a
|
|
68
|
+
# link to any file on the machine, and the carve-out would be appended to it; a file
|
|
69
|
+
# in a .git is no file of the repository's either, but git's own hooks and config,
|
|
70
|
+
# and a line appended to a hook runs as a command the next time git runs it. The case
|
|
71
|
+
# matters because a case-insensitive file system (macOS) opens .git for .GIT. The TS
|
|
72
|
+
# twin, ensureDevflowGitignore in src/targets/claude-code/post-install.ts, applies the
|
|
73
|
+
# same rule the same way: the link is followed one hop at a time, at most 40 hops, and
|
|
74
|
+
# only its last folder is resolved physically, so a missing file inside the project
|
|
75
|
+
# is created there as before.
|
|
76
|
+
#
|
|
61
77
|
# Usage: source ensure-root-gitignore "$PROJECT_ROOT"
|
|
62
78
|
# Sourced helper: uses `return` (never exit), _ERG_-prefixed locals (never clobbers
|
|
63
79
|
# caller vars).
|
|
@@ -68,6 +84,76 @@ _ERG_DEVFLOW_DIR="$1/.devflow"
|
|
|
68
84
|
_ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v6"
|
|
69
85
|
_ERG_GITIGNORE="$1/.gitignore"
|
|
70
86
|
|
|
87
|
+
# _erg_resolve_inside <root> <link> — follow the symbolic link <link> one hop at a
|
|
88
|
+
# time, at most 40, and set _ERG_RESOLVED to the file it leads to, its folder
|
|
89
|
+
# resolved physically. Returns 0 when that file lies inside <root> and no part of its
|
|
90
|
+
# path below <root> is named .git in any letter case (D-GITIGNORE-LINK-INSIDE);
|
|
91
|
+
# returns 1, with _ERG_RESOLVED empty, when it does not and when the link cannot be
|
|
92
|
+
# followed: a read that fails, an empty or folder-like target, a chain longer than
|
|
93
|
+
# 40, a folder that does not resolve. A predicate: call it in an `if`, never as a
|
|
94
|
+
# plain statement under `set -e`. Relative targets are joined to the folder the link
|
|
95
|
+
# sits in as spelled, never normalised, so `..` is resolved by the file system as the
|
|
96
|
+
# write itself would resolve it. Each hop forks one readlink; `cd -P` and $PWD are
|
|
97
|
+
# builtins, and the caller's folder is restored before returning.
|
|
98
|
+
_erg_resolve_inside() {
|
|
99
|
+
local _root="$1" _cur="$2" _target="" _dir="" _base="" _root_real="" _hops=0 _back="$PWD"
|
|
100
|
+
_ERG_RESOLVED=''
|
|
101
|
+
# Absolute from the start, so no path is read as an option, searched for along
|
|
102
|
+
# CDPATH, or taken relative to a folder this function moved into.
|
|
103
|
+
case "$_root" in /*) ;; *) _root="$_back/$_root" ;; esac
|
|
104
|
+
case "$_cur" in /*) ;; *) _cur="$_back/$_cur" ;; esac
|
|
105
|
+
while [ -L "$_cur" ]; do
|
|
106
|
+
[ "$_hops" -lt 40 ] || return 1
|
|
107
|
+
# The `x` keeps a target's own trailing newlines from command substitution; the
|
|
108
|
+
# one newline readlink adds is then removed.
|
|
109
|
+
_target="$(readlink "$_cur" 2>/dev/null && printf x)"
|
|
110
|
+
case "$_target" in *x) _target="${_target%x}" ;; *) return 1 ;; esac
|
|
111
|
+
_target="${_target%$'\n'}"
|
|
112
|
+
case "$_target" in
|
|
113
|
+
'') return 1 ;;
|
|
114
|
+
/*) _cur="$_target" ;;
|
|
115
|
+
*) _cur="${_cur%/*}/$_target" ;;
|
|
116
|
+
esac
|
|
117
|
+
_hops=$((_hops + 1))
|
|
118
|
+
done
|
|
119
|
+
_base="${_cur##*/}"
|
|
120
|
+
_dir="${_cur%/*}"
|
|
121
|
+
case "$_base" in ''|.|..) return 1 ;; esac
|
|
122
|
+
[ -n "$_dir" ] || _dir=/
|
|
123
|
+
if cd -P "$_root" 2>/dev/null; then
|
|
124
|
+
_root_real="${PWD%/}"
|
|
125
|
+
if cd -P "$_dir" 2>/dev/null; then _ERG_RESOLVED="${PWD%/}/$_base"; fi
|
|
126
|
+
fi
|
|
127
|
+
cd "$_back" 2>/dev/null || true
|
|
128
|
+
case "$_ERG_RESOLVED" in
|
|
129
|
+
'') ;;
|
|
130
|
+
"$_root_real"/?*)
|
|
131
|
+
# Each part below the root is tested whole, in any letter case: `cd -P` keeps
|
|
132
|
+
# the spelling the link gave, and a case-insensitive file system opens `.git`
|
|
133
|
+
# for `.GIT`.
|
|
134
|
+
case "/${_ERG_RESOLVED#"$_root_real"/}/" in
|
|
135
|
+
*/.[Gg][Ii][Tt]/*) ;;
|
|
136
|
+
*) return 0 ;;
|
|
137
|
+
esac
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
_ERG_RESOLVED=''
|
|
141
|
+
return 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# D-GITIGNORE-LINK-INSIDE (above): checked before the fast path, so a link that leads
|
|
145
|
+
# elsewhere is neither read nor written. A regular .gitignore costs one builtin test.
|
|
146
|
+
if [ -L "$_ERG_GITIGNORE" ]; then
|
|
147
|
+
if _erg_resolve_inside "$1" "$_ERG_GITIGNORE"; then
|
|
148
|
+
_ERG_GITIGNORE="$_ERG_RESOLVED"
|
|
149
|
+
else
|
|
150
|
+
if declare -F log >/dev/null 2>&1; then
|
|
151
|
+
log "Skipped: $_ERG_GITIGNORE is a symbolic link that leads outside the project, into a .git, or nowhere; nothing was written through it"
|
|
152
|
+
fi
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
71
157
|
# Whole-line, whitespace-tolerant matchers. `*` and `.` are escaped so the ERE
|
|
72
158
|
# matches the literal gitignore patterns.
|
|
73
159
|
_ERG_RE_OPTOUT='^[[:space:]]*/\.devflow/[[:space:]]*$'
|
|
@@ -208,18 +294,30 @@ else
|
|
|
208
294
|
# Upgrade our legacy wholesale entry: strip the bare `.devflow/` line and our old
|
|
209
295
|
# comment first, then fall through to the shared append below. Portable (grep
|
|
210
296
|
# filter + mv; no sed -i, which differs on BSD/GNU).
|
|
211
|
-
# The
|
|
297
|
+
# The filter's own status is deliberately ignored: grep -v exits 1 when it
|
|
212
298
|
# emits no lines, which is the correct outcome for a .gitignore whose only
|
|
213
|
-
# content was the legacy entry
|
|
299
|
+
# content was the legacy entry, so `true` ends the group and the subshell fails
|
|
300
|
+
# only when the copy cannot be created. The copy is created only where nothing
|
|
301
|
+
# stands at its name: builtin tests come first, so an entry already there, a
|
|
302
|
+
# link to a FIFO or a device included, is never opened (noclobber alone would
|
|
303
|
+
# open one, as no regular file stands there), and noclobber then makes the
|
|
304
|
+
# create itself exclusive (D-HOOKS-NO-SYMLINK, git-marker). Such an entry is
|
|
305
|
+
# removed instead; the upgrade then waits for a later run, and the skip is
|
|
306
|
+
# logged once.
|
|
214
307
|
_ERG_TMP="$_ERG_GITIGNORE.devflow-tmp.$$"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
308
|
+
if [ ! -e "$_ERG_TMP" ] && [ ! -L "$_ERG_TMP" ] && (set -o noclobber; {
|
|
309
|
+
grep -vE "$_ERG_RE_LEGACY" "$_ERG_GITIGNORE" \
|
|
310
|
+
| grep -vF '# Devflow runtime data (local by default; remove to share via git)'
|
|
311
|
+
true
|
|
312
|
+
} > "$_ERG_TMP") 2>/dev/null \
|
|
313
|
+
&& [ -f "$_ERG_TMP" ] && mv "$_ERG_TMP" "$_ERG_GITIGNORE"; then
|
|
219
314
|
:
|
|
220
315
|
else
|
|
221
|
-
rm -f "$_ERG_TMP" 2>/dev/null
|
|
316
|
+
rm -f "$_ERG_TMP" 2>/dev/null || true
|
|
222
317
|
_ERG_MODE=fail
|
|
318
|
+
if declare -F log >/dev/null 2>&1; then
|
|
319
|
+
log "Legacy .devflow/ entry not upgraded: its copy could not be written ($_ERG_TMP); a later run retries"
|
|
320
|
+
fi
|
|
223
321
|
fi
|
|
224
322
|
fi
|
|
225
323
|
fi
|
|
@@ -231,7 +329,8 @@ fi
|
|
|
231
329
|
# unchanged, and when the run ends on a last line with no newline, one is added
|
|
232
330
|
# before <text>. Each read ends in a trailing `x`, so command substitution cannot
|
|
233
331
|
# strip the file's own trailing newlines. The file is rewritten in place with one
|
|
234
|
-
# `>`, as the TS twin's writeFile does
|
|
332
|
+
# `>`, as the TS twin's writeFile does; a .gitignore linked to a file inside the
|
|
333
|
+
# project is rewritten at that file, so the link stays (D-GITIGNORE-LINK-INSIDE).
|
|
235
334
|
# Returns non-zero, having written nothing, when the anchor line cannot be found.
|
|
236
335
|
_erg_insert_in_block() {
|
|
237
336
|
local _anchor="$1" _run="$2" _text="$3" _hit="" _n="" _next="" _line="" _i=0 _head="" _tail=""
|
|
@@ -284,9 +383,22 @@ case "$_ERG_MODE" in
|
|
|
284
383
|
;;
|
|
285
384
|
esac
|
|
286
385
|
|
|
287
|
-
# On success, stamp the current-format v6 marker and drop legacy markers.
|
|
386
|
+
# On success, stamp the current-format v6 marker and drop legacy markers. The marker
|
|
387
|
+
# is created only where nothing stands, never `touch`ed: `touch` follows a symbolic
|
|
388
|
+
# link committed at its path (D-HOOKS-NO-SYMLINK, git-marker), and so does
|
|
389
|
+
# noclobber's create when the link leads to a FIFO, where it would wait at every
|
|
390
|
+
# session start and prompt, or to a device. So builtin tests come first: a link
|
|
391
|
+
# there is left as it was and the skip logged once, a marker already there is all
|
|
392
|
+
# the fast path needs, and noclobber makes the create itself exclusive. A `.devflow`
|
|
393
|
+
# that is itself a link is refused by both callers before they reach this file.
|
|
288
394
|
[ "$_ERG_OK" = 1 ] && {
|
|
289
|
-
|
|
395
|
+
if [ -L "$_ERG_MARKER" ]; then
|
|
396
|
+
if declare -F log >/dev/null 2>&1; then
|
|
397
|
+
log "Marker not stamped: $_ERG_MARKER is a symbolic link"
|
|
398
|
+
fi
|
|
399
|
+
elif [ ! -e "$_ERG_MARKER" ]; then
|
|
400
|
+
(set -o noclobber; : > "$_ERG_MARKER") 2>/dev/null || true
|
|
401
|
+
fi
|
|
290
402
|
_erg_drop_legacy_markers
|
|
291
403
|
}
|
|
292
404
|
return 0
|
|
@@ -62,3 +62,74 @@ df_is_project_root() {
|
|
|
62
62
|
[ -n "$_root_real" ] || return 1
|
|
63
63
|
[ "$_root_real" != "$_home_real" ]
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
# df_no_symlink_below <root> <path>... — returns 0 when no <path>, and no folder
|
|
67
|
+
# between <root> and it, is a symbolic link. Returns 1 when one is, and for any
|
|
68
|
+
# call it cannot vouch for: no <path>, an empty <root>, a <path> not below <root>,
|
|
69
|
+
# or a <path> with an empty, `.` or `..` part or more than 64 parts below <root>.
|
|
70
|
+
# A predicate: call it in an `if` or an `||` list, never as a plain statement
|
|
71
|
+
# under `set -e`.
|
|
72
|
+
#
|
|
73
|
+
# D-HOOKS-NO-SYMLINK: a hook writes under a project's `.devflow/`, and reads a file
|
|
74
|
+
# there into the session context, a backup, a prompt or an agent directive, only
|
|
75
|
+
# where nothing on the way down from the root is a symbolic link. A repository can
|
|
76
|
+
# commit a link anywhere in its own `.devflow/`, and the hooks' shell commands follow
|
|
77
|
+
# one. `>>` and `touch` write through a linked file and `mkdir -p` creates folders
|
|
78
|
+
# inside a linked folder, so a linked queue or folder would put captured
|
|
79
|
+
# conversation text into whatever file the link names. `head`, `sed`, `read` and
|
|
80
|
+
# `jq` read through one, so a linked working memory, backup or decisions file would
|
|
81
|
+
# put whatever file the link names into the session, the memory refresh's prompt or
|
|
82
|
+
# the backup the next session restores, with no one choosing to read it. Each hook
|
|
83
|
+
# checks every path this rule covers: a refused write is skipped and a refused
|
|
84
|
+
# read is treated as an absent file (df_file_below below), each logged once. A
|
|
85
|
+
# rename's target is one of those paths: `mv` moves a file into the folder a link
|
|
86
|
+
# at its target names, so a link there, to anything, refuses the rename.
|
|
87
|
+
#
|
|
88
|
+
# Only the part of each path below <root> is checked, never the root or a folder
|
|
89
|
+
# above it: the root is where the session runs or where git keeps the ledger,
|
|
90
|
+
# chosen by the user and not by what the repository holds, and a folder above it
|
|
91
|
+
# may legitimately be a link (on macOS /tmp and /var lead into /private). The check
|
|
92
|
+
# runs just before the write or the read, so a process that swaps a link in between
|
|
93
|
+
# is not stopped; such a process already runs as the user.
|
|
94
|
+
#
|
|
95
|
+
# Zero forks: builtin `[ -L ]` tests and parameter expansion only.
|
|
96
|
+
df_no_symlink_below() {
|
|
97
|
+
local _root="$1" _path="" _rest="" _seg="" _at="" _n=0
|
|
98
|
+
[ -n "$_root" ] || return 1
|
|
99
|
+
shift
|
|
100
|
+
[ "$#" -gt 0 ] || return 1
|
|
101
|
+
for _path in "$@"; do
|
|
102
|
+
case "$_path" in
|
|
103
|
+
"$_root"/?*) _rest="${_path#"$_root"/}" ;;
|
|
104
|
+
*) return 1 ;;
|
|
105
|
+
esac
|
|
106
|
+
_at="$_root"
|
|
107
|
+
_n=0
|
|
108
|
+
while [ -n "$_rest" ]; do
|
|
109
|
+
[ "$_n" -lt 64 ] || return 1
|
|
110
|
+
_seg="${_rest%%/*}"
|
|
111
|
+
if [ "$_seg" = "$_rest" ]; then _rest=""; else _rest="${_rest#*/}"; fi
|
|
112
|
+
case "$_seg" in ''|.|..) return 1 ;; esac
|
|
113
|
+
_at="$_at/$_seg"
|
|
114
|
+
if [ -L "$_at" ]; then return 1; fi
|
|
115
|
+
_n=$((_n + 1))
|
|
116
|
+
done
|
|
117
|
+
done
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# df_file_below <root> <path> — returns 0 when <path> is a regular file that
|
|
122
|
+
# df_no_symlink_below admits, so a hook may read it. Returns 1 when no regular file
|
|
123
|
+
# stands there, and when a symbolic link sits on its path: the file is then treated
|
|
124
|
+
# as absent (D-HOOKS-NO-SYMLINK, above), and that refusal is logged once through the
|
|
125
|
+
# caller's log(), where it defines one. `[ -f ]` follows a link, so a dangling link
|
|
126
|
+
# or a link to a folder reads as absent with nothing logged: nothing would be read
|
|
127
|
+
# through it. A predicate, like df_no_symlink_below; it forks nothing unless it logs.
|
|
128
|
+
df_file_below() {
|
|
129
|
+
[ -f "$2" ] || return 1
|
|
130
|
+
if df_no_symlink_below "$1" "$2"; then return 0; fi
|
|
131
|
+
if declare -F log >/dev/null 2>&1; then
|
|
132
|
+
log "Skipped: a symbolic link sits on the path to $2; treated as absent"
|
|
133
|
+
fi
|
|
134
|
+
return 1
|
|
135
|
+
}
|
|
@@ -11,18 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Operations:
|
|
13
13
|
// get-field <field> [default] Read field from stdin JSON
|
|
14
|
-
//
|
|
15
|
-
// compact Compact stdin JSON to single line
|
|
16
|
-
// construct <json-template> [--arg k v] Build JSON object with args
|
|
17
|
-
// update-field <field> <value> [--json] Set field on stdin JSON (--json parses value)
|
|
18
|
-
// update-fields <json-patches> Apply multiple field updates from stdin JSON
|
|
14
|
+
// get-string-field <field> Read field from stdin JSON only when it is a string
|
|
19
15
|
// extract-cwd-field <field> Extract cwd + arbitrary field, SOH-byte delimited
|
|
20
|
-
// extract-text-messages Extract text content from Claude message format
|
|
21
|
-
// merge-evidence Flatten, dedupe, limit to 10 from stdin JSON
|
|
22
|
-
// slurp-sort <field> [limit] Read stdin JSONL, sort by field desc, limit results
|
|
23
|
-
// slurp-cap <field> [limit] Read stdin JSONL, sort by field desc, output limit lines
|
|
24
|
-
// array-length <path> Get length of array at dotted path in stdin JSON
|
|
25
|
-
// array-item <path> <index> Get item at index from array at path in stdin JSON
|
|
26
16
|
// session-output <context> Build SessionStart output envelope
|
|
27
17
|
// prompt-output <context> Build UserPromptSubmit output envelope
|
|
28
18
|
// backup-construct Build pre-compact backup JSON from --arg pairs
|
|
@@ -165,31 +155,15 @@ function getNestedField(obj, field) {
|
|
|
165
155
|
return current;
|
|
166
156
|
}
|
|
167
157
|
|
|
168
|
-
/** The JSON values of a JSONL text's lines; a line that does not parse is skipped. */
|
|
169
|
-
function parseJsonlText(text) {
|
|
170
|
-
const lines = text.split('\n').filter(Boolean);
|
|
171
|
-
return lines.map(l => {
|
|
172
|
-
try { return JSON.parse(l); } catch { return null; }
|
|
173
|
-
}).filter(Boolean);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
158
|
function parseArgs(argList) {
|
|
177
159
|
const result = {};
|
|
178
|
-
const jsonArgs = {};
|
|
179
160
|
for (let i = 0; i < argList.length; i++) {
|
|
180
161
|
if (argList[i] === '--arg' && i + 2 < argList.length) {
|
|
181
162
|
result[argList[i + 1]] = argList[i + 2];
|
|
182
163
|
i += 2;
|
|
183
|
-
} else if (argList[i] === '--argjson' && i + 2 < argList.length) {
|
|
184
|
-
try {
|
|
185
|
-
jsonArgs[argList[i + 1]] = JSON.parse(argList[i + 2]);
|
|
186
|
-
} catch {
|
|
187
|
-
jsonArgs[argList[i + 1]] = argList[i + 2];
|
|
188
|
-
}
|
|
189
|
-
i += 2;
|
|
190
164
|
}
|
|
191
165
|
}
|
|
192
|
-
return
|
|
166
|
+
return result;
|
|
193
167
|
}
|
|
194
168
|
|
|
195
169
|
// ---------------------------------------------------------------------------
|
|
@@ -278,7 +252,8 @@ function heartbeat(root) {
|
|
|
278
252
|
// The learning ops run from the project root and take no path to a learning file:
|
|
279
253
|
// each builds its paths from the current directory. Every op that writes takes the
|
|
280
254
|
// store's one learning lock through withDecisionsLock (D-ONE-LEARNING-LOCK) and
|
|
281
|
-
// refuses, creating nothing, when .devflow/learning/ is absent (D-NO-STRAY-TREE)
|
|
255
|
+
// refuses, creating nothing, when .devflow/learning/ is absent (D-NO-STRAY-TREE)
|
|
256
|
+
// or when it, or .devflow, is a symbolic link (D-NO-LINKED-TREE).
|
|
282
257
|
// A locked body returns its Result; emit prints it once the lock is released.
|
|
283
258
|
if (require.main === module) {
|
|
284
259
|
try {
|
|
@@ -293,54 +268,15 @@ try {
|
|
|
293
268
|
break;
|
|
294
269
|
}
|
|
295
270
|
|
|
296
|
-
case '
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} catch {
|
|
303
|
-
process.exit(1);
|
|
304
|
-
}
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
case 'compact': {
|
|
271
|
+
case 'get-string-field': {
|
|
272
|
+
// The typed read: get-field stringifies, so the number 42 and the string
|
|
273
|
+
// "42" are the same to it. Here a JSON string prints byte-exact (no
|
|
274
|
+
// trailing newline added, so the caller sees the value's own) and every
|
|
275
|
+
// other type, an absent field, or a string holding a NUL (no shell
|
|
276
|
+
// variable can carry one) prints nothing.
|
|
309
277
|
const input = JSON.parse(readStdin());
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
case 'construct': {
|
|
315
|
-
// Build JSON from --arg/--argjson pairs
|
|
316
|
-
const template = parseArgs(args);
|
|
317
|
-
console.log(JSON.stringify(template));
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
case 'update-field': {
|
|
322
|
-
const input = JSON.parse(readStdin());
|
|
323
|
-
const field = args[0];
|
|
324
|
-
const value = args[1];
|
|
325
|
-
const isJson = args[2] === '--json';
|
|
326
|
-
input[field] = isJson ? JSON.parse(value) : value;
|
|
327
|
-
console.log(JSON.stringify(input));
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
case 'update-fields': {
|
|
332
|
-
// Read stdin JSON, apply field updates from args: field1=val1 field2=val2
|
|
333
|
-
const input = JSON.parse(readStdin());
|
|
334
|
-
for (const arg of args) {
|
|
335
|
-
const eqIdx = arg.indexOf('=');
|
|
336
|
-
if (eqIdx > 0) {
|
|
337
|
-
const key = arg.slice(0, eqIdx);
|
|
338
|
-
const val = arg.slice(eqIdx + 1);
|
|
339
|
-
// Try to parse as JSON, fall back to string
|
|
340
|
-
try { input[key] = JSON.parse(val); } catch { input[key] = val; }
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
console.log(JSON.stringify(input));
|
|
278
|
+
const val = getNestedField(input, args[0]);
|
|
279
|
+
if (typeof val === 'string' && !val.includes('\0')) process.stdout.write(val);
|
|
344
280
|
break;
|
|
345
281
|
}
|
|
346
282
|
|
|
@@ -356,75 +292,6 @@ try {
|
|
|
356
292
|
break;
|
|
357
293
|
}
|
|
358
294
|
|
|
359
|
-
case 'extract-text-messages': {
|
|
360
|
-
const input = JSON.parse(readStdin());
|
|
361
|
-
const content = input?.message?.content;
|
|
362
|
-
if (typeof content === 'string') {
|
|
363
|
-
console.log(content);
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
if (!Array.isArray(content)) {
|
|
367
|
-
console.log('');
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
const texts = content
|
|
371
|
-
.filter(c => c.type === 'text')
|
|
372
|
-
.map(c => c.text);
|
|
373
|
-
console.log(texts.join('\n'));
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
case 'merge-evidence': {
|
|
378
|
-
const input = JSON.parse(readStdin());
|
|
379
|
-
// input is [[old_evidence], [new_evidence]] — flatten, dedupe, limit
|
|
380
|
-
const flat = input.flat();
|
|
381
|
-
const unique = [...new Set(flat)];
|
|
382
|
-
console.log(JSON.stringify(unique.slice(0, 10)));
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
case 'slurp-sort': {
|
|
387
|
-
const field = args[0];
|
|
388
|
-
const limit = parseInt(args[1]) || 30;
|
|
389
|
-
const parsed = parseJsonlText(readStdin());
|
|
390
|
-
parsed.sort((a, b) => (b[field] || 0) - (a[field] || 0));
|
|
391
|
-
console.log(JSON.stringify(parsed.slice(0, limit)));
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
case 'slurp-cap': {
|
|
396
|
-
// Read JSONL, sort by field desc, output top N as JSONL (one per line)
|
|
397
|
-
const field = args[0];
|
|
398
|
-
const limit = parseInt(args[1]) || 100;
|
|
399
|
-
const parsed = parseJsonlText(readStdin());
|
|
400
|
-
parsed.sort((a, b) => (b[field] || 0) - (a[field] || 0));
|
|
401
|
-
for (const item of parsed.slice(0, limit)) {
|
|
402
|
-
console.log(JSON.stringify(item));
|
|
403
|
-
}
|
|
404
|
-
break;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
case 'array-length': {
|
|
408
|
-
const input = JSON.parse(readStdin());
|
|
409
|
-
const dotPath = args[0];
|
|
410
|
-
const arr = getNestedField(input, dotPath);
|
|
411
|
-
console.log(Array.isArray(arr) ? arr.length : 0);
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
case 'array-item': {
|
|
416
|
-
const input = JSON.parse(readStdin());
|
|
417
|
-
const dotPath = args[0];
|
|
418
|
-
const index = parseInt(args[1]);
|
|
419
|
-
const arr = getNestedField(input, dotPath);
|
|
420
|
-
if (Array.isArray(arr) && index >= 0 && index < arr.length) {
|
|
421
|
-
console.log(JSON.stringify(arr[index]));
|
|
422
|
-
} else {
|
|
423
|
-
console.log('null');
|
|
424
|
-
}
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
295
|
case 'session-output': {
|
|
429
296
|
const ctx = args[0];
|
|
430
297
|
console.log(JSON.stringify({
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
# JSON parsing helper — tries jq, falls back to node json-helper.
|
|
3
|
+
# JSON parsing helper — tries jq, falls back to node json-helper.cjs
|
|
4
4
|
# Usage: source "$SCRIPT_DIR/json-parse"
|
|
5
5
|
#
|
|
6
6
|
# After sourcing, check $_JSON_AVAILABLE before using json_* functions.
|
|
7
7
|
# All functions read from stdin unless noted otherwise.
|
|
8
|
+
# Where a jq path discards stderr, its node fallback does too, so input that
|
|
9
|
+
# cannot be parsed fails silently on either backend.
|
|
8
10
|
|
|
9
11
|
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
12
|
_JSON_HELPER="$_SCRIPT_DIR/json-helper.cjs"
|
|
@@ -28,134 +30,42 @@ json_field() {
|
|
|
28
30
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
29
31
|
jq -r ".$field // \"$default\"" 2>/dev/null
|
|
30
32
|
else
|
|
31
|
-
node "$_JSON_HELPER" get-field "$field" "$default"
|
|
33
|
+
node "$_JSON_HELPER" get-field "$field" "$default" 2>/dev/null
|
|
32
34
|
fi
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
# Extract a field from a JSON file. Usage: json_field_file "/path/to/file.json" "field" "default"
|
|
36
38
|
# Note: uses if/then/else to preserve boolean false (jq // operator would replace false with default)
|
|
37
39
|
# The node fallback reads the file on stdin: json-helper takes no file path.
|
|
40
|
+
# Both paths are silent on stderr: an unparseable or missing file yields empty
|
|
41
|
+
# output and a failing status, as jq does with its errors discarded. The node
|
|
42
|
+
# path redirects stderr BEFORE stdin, so the shell's own error for a `< "$file"`
|
|
43
|
+
# that cannot open is discarded too.
|
|
38
44
|
json_field_file() {
|
|
39
45
|
local file="$1" field="$2" default="${3:-}"
|
|
40
46
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
41
47
|
jq -r "if (.$field | type) == \"null\" then \"$default\" else (.$field | tostring) end" "$file" 2>/dev/null
|
|
42
48
|
else
|
|
43
|
-
node "$_JSON_HELPER" get-field "$field" "$default" < "$file"
|
|
49
|
+
node "$_JSON_HELPER" get-field "$field" "$default" 2>/dev/null < "$file"
|
|
44
50
|
fi
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Compact stdin JSON to single line. Usage: echo '{ "k": "v" }' | json_compact
|
|
61
|
-
json_compact() {
|
|
62
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
63
|
-
jq -c '.' 2>/dev/null
|
|
64
|
-
else
|
|
65
|
-
node "$_JSON_HELPER" compact
|
|
66
|
-
fi
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
# --- Construction ---
|
|
70
|
-
|
|
71
|
-
# Build a JSON object from --arg/--argjson pairs.
|
|
72
|
-
# Usage: json_construct --arg key1 val1 --arg key2 val2
|
|
73
|
-
json_construct() {
|
|
74
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
75
|
-
# Build jq expression from args
|
|
76
|
-
local jq_args=()
|
|
77
|
-
local fields=""
|
|
78
|
-
local i=0
|
|
79
|
-
while [ $i -lt $# ]; do
|
|
80
|
-
local flag="${!((i+1))}"
|
|
81
|
-
if [ "$flag" = "--arg" ]; then
|
|
82
|
-
local name="${!((i+2))}"
|
|
83
|
-
local val="${!((i+3))}"
|
|
84
|
-
jq_args+=(--arg "$name" "$val")
|
|
85
|
-
if [ -n "$fields" ]; then fields="$fields, "; fi
|
|
86
|
-
fields="${fields}${name}: \$${name}"
|
|
87
|
-
i=$((i + 3))
|
|
88
|
-
elif [ "$flag" = "--argjson" ]; then
|
|
89
|
-
local name="${!((i+2))}"
|
|
90
|
-
local val="${!((i+3))}"
|
|
91
|
-
jq_args+=(--argjson "$name" "$val")
|
|
92
|
-
if [ -n "$fields" ]; then fields="$fields, "; fi
|
|
93
|
-
fields="${fields}${name}: \$${name}"
|
|
94
|
-
i=$((i + 3))
|
|
95
|
-
else
|
|
96
|
-
i=$((i + 1))
|
|
97
|
-
fi
|
|
98
|
-
done
|
|
99
|
-
jq -n -c "${jq_args[@]}" "{$fields}" 2>/dev/null
|
|
100
|
-
else
|
|
101
|
-
node "$_JSON_HELPER" construct "$@"
|
|
102
|
-
fi
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
# --- Field updates ---
|
|
106
|
-
|
|
107
|
-
# Update a field on stdin JSON. Usage: echo '{"k":"v"}' | json_update_field "k" "new_v"
|
|
108
|
-
json_update_field() {
|
|
109
|
-
local field="$1" value="$2"
|
|
110
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
111
|
-
jq -c --arg v "$value" ".$field = \$v" 2>/dev/null
|
|
112
|
-
else
|
|
113
|
-
node "$_JSON_HELPER" update-field "$field" "$value"
|
|
114
|
-
fi
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
# Update a field with a JSON value. Usage: echo '{}' | json_update_field_json "k" '42'
|
|
118
|
-
json_update_field_json() {
|
|
119
|
-
local field="$1" value="$2"
|
|
53
|
+
# The typed read of a field from a JSON file. Usage: json_string_field_file "/path/to/file.json" "field"
|
|
54
|
+
# json_field_file stringifies, so the number 42 and the string "42" read the same;
|
|
55
|
+
# this prints the field only when it is a JSON string, and nothing for any other
|
|
56
|
+
# type (a number, boolean, object, array or null), an absent field, or a string
|
|
57
|
+
# holding a NUL, which no shell variable can carry. The string is printed
|
|
58
|
+
# byte-exact with no newline added, so a caller that must see the value's own
|
|
59
|
+
# trailing newline appends a sentinel before the command substitution strips it:
|
|
60
|
+
# v=$(json_string_field_file "$file" "k"; printf x); v=${v%x}
|
|
61
|
+
# Silent on stderr and failing on an unparseable or missing file, as json_field_file
|
|
62
|
+
# is, and read the same way on both backends (the node path reads the file on stdin).
|
|
63
|
+
json_string_field_file() {
|
|
64
|
+
local file="$1" field="$2"
|
|
120
65
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
121
|
-
jq -
|
|
66
|
+
jq -j ".$field | select(type == \"string\" and all(explode[]; . != 0))" "$file" 2>/dev/null
|
|
122
67
|
else
|
|
123
|
-
node "$_JSON_HELPER"
|
|
124
|
-
fi
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
# --- Slurp operations ---
|
|
128
|
-
|
|
129
|
-
# Slurp JSONL file, sort by field desc, output as JSONL (one line per entry).
|
|
130
|
-
# Usage: json_slurp_cap "file.jsonl" "confidence" 100
|
|
131
|
-
json_slurp_cap() {
|
|
132
|
-
local file="$1" field="$2" limit="${3:-100}"
|
|
133
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
134
|
-
jq -c '.' "$file" | jq -s "sort_by(.$field) | reverse | .[0:$limit][]" 2>/dev/null
|
|
135
|
-
else
|
|
136
|
-
node "$_JSON_HELPER" slurp-cap "$field" "$limit" < "$file"
|
|
137
|
-
fi
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# --- Array operations ---
|
|
141
|
-
|
|
142
|
-
# Get length of array at path. Usage: echo '{"obs":[1,2]}' | json_array_length "observations"
|
|
143
|
-
json_array_length() {
|
|
144
|
-
local path="$1"
|
|
145
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
146
|
-
jq ".$path | length" 2>/dev/null
|
|
147
|
-
else
|
|
148
|
-
node "$_JSON_HELPER" array-length "$path"
|
|
149
|
-
fi
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
# Get item at index from array. Usage: echo '{"obs":[{},{}]}' | json_array_item "observations" 0
|
|
153
|
-
json_array_item() {
|
|
154
|
-
local path="$1" index="$2"
|
|
155
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
156
|
-
jq -c ".${path}[$index]" 2>/dev/null
|
|
157
|
-
else
|
|
158
|
-
node "$_JSON_HELPER" array-item "$path" "$index"
|
|
68
|
+
node "$_JSON_HELPER" get-string-field "$field" 2>/dev/null < "$file"
|
|
159
69
|
fi
|
|
160
70
|
}
|
|
161
71
|
|
|
@@ -176,7 +86,7 @@ json_extract_cwd_field() {
|
|
|
176
86
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
177
87
|
jq -r --arg f "$field" '(.cwd // "") + "\u0001" + (.[$f] // "")' 2>/dev/null
|
|
178
88
|
else
|
|
179
|
-
node "$_JSON_HELPER" extract-cwd-field "$field"
|
|
89
|
+
node "$_JSON_HELPER" extract-cwd-field "$field" 2>/dev/null
|
|
180
90
|
fi
|
|
181
91
|
}
|
|
182
92
|
|
|
@@ -186,21 +96,6 @@ json_extract_cwd_prompt() {
|
|
|
186
96
|
json_extract_cwd_field "prompt"
|
|
187
97
|
}
|
|
188
98
|
|
|
189
|
-
# --- Transcript extraction ---
|
|
190
|
-
|
|
191
|
-
# Extract text messages from Claude message JSON. Usage: printf '%s\n' '{"message":...}' | json_extract_messages
|
|
192
|
-
json_extract_messages() {
|
|
193
|
-
if [ "$_HAS_JQ" = "true" ]; then
|
|
194
|
-
jq -r 'if .message.content then
|
|
195
|
-
if (.message.content | type) == "string" then .message.content
|
|
196
|
-
else [.message.content[] | select(.type == "text") | .text] | join("\n")
|
|
197
|
-
end
|
|
198
|
-
else "" end' 2>/dev/null
|
|
199
|
-
else
|
|
200
|
-
node "$_JSON_HELPER" extract-text-messages
|
|
201
|
-
fi
|
|
202
|
-
}
|
|
203
|
-
|
|
204
99
|
# --- Hook output envelopes ---
|
|
205
100
|
|
|
206
101
|
# Build SessionStart output. Usage: json_session_output "$CONTEXT"
|