@indigoai-us/hq-cli 5.345.88 → 5.345.90
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 +8 -0
- package/assets/bot-workers/setup/skills/your-business.md +46 -12
- package/assets/bot-workers/setup/worker.yaml +152 -9
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +107 -21
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +10 -3
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +36 -5
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +18 -3
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +74 -26
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +10 -1
- package/assets/scaffold/core/scripts/hq-job-probe.sh +10 -3
- package/assets/scaffold/core/scripts/jobs-validate.sh +10 -3
- package/assets/scaffold/core/scripts/lib/delegate-brainstorm.sh +120 -0
- package/dist/commands/core-delegate-brainstorm.d.ts +9 -0
- package/dist/commands/core-delegate-brainstorm.js +161 -0
- package/dist/commands/core-delegate-w1b.js +134 -40
- package/dist/commands/core-delegate.d.ts +2 -0
- package/dist/commands/core-delegate.js +56 -29
- package/dist/commands/core.js +1 -0
- package/dist/lib/bot/dm-history.d.ts +37 -0
- package/dist/lib/bot/dm-history.js +126 -0
- package/dist/lib/bot/paths.d.ts +6 -0
- package/dist/lib/bot/paths.js +15 -0
- package/dist/lib/bot/prompt.js +1 -0
- package/dist/lib/bot/run.d.ts +5 -3
- package/dist/lib/bot/run.js +198 -101
- package/package.json +1 -1
|
@@ -34,6 +34,8 @@ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
|
34
34
|
|
|
35
35
|
# shellcheck source=lib/secret-patterns.sh
|
|
36
36
|
. "$SCRIPT_DIR/lib/secret-patterns.sh"
|
|
37
|
+
# shellcheck source=lib/delegate-brainstorm.sh
|
|
38
|
+
. "$SCRIPT_DIR/lib/delegate-brainstorm.sh"
|
|
37
39
|
|
|
38
40
|
usage() { sed -n '3,27p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
39
41
|
die() { echo "hq-delegate-send: $*" >&2; exit 1; }
|
|
@@ -100,12 +102,25 @@ MODE="$(jq -r '.mode // "transfer"' "$MANIFEST")"
|
|
|
100
102
|
[ -n "$PROJECT" ] || die "manifest has no project.name"
|
|
101
103
|
[ -n "$PRINCIPAL" ] || die "manifest has no to.principal"
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
STAGE="$(jq -r '.project.stage // "prd"' "$MANIFEST")"
|
|
106
|
+
PRD_REL="$(jq -r '.project.prdPath // empty' "$MANIFEST")"
|
|
107
|
+
PRD_ABS="${PRD_REL:+$HQ_ROOT/$PRD_REL}"
|
|
108
|
+
BRAINSTORM_ABS="$HQ_ROOT/companies/$COMPANY/projects/$PROJECT/brainstorm.md"
|
|
104
109
|
|
|
105
110
|
# --- assemble prompt sections from the manifest (single source of truth) -----
|
|
106
111
|
|
|
107
112
|
GOAL="" STATE_LINE="" NEXT_STEPS=""
|
|
108
|
-
|
|
113
|
+
DOSSIER_LINE="Start with the project dossier: \`companies/$COMPANY/projects/$PROJECT/\` now contains the PRD (\`prd.json\`), a full brief (\`README.md\` / the DM's details pane), and the delegation journal."
|
|
114
|
+
if [ "$STAGE" = brainstorm ]; then
|
|
115
|
+
# Brainstorm stage: no PRD, no stories. The next steps are to read the
|
|
116
|
+
# brainstorm and turn it into a PRD with /plan.
|
|
117
|
+
[ -f "$BRAINSTORM_ABS" ] && GOAL="$(hq_brainstorm_summary "$BRAINSTORM_ABS")"
|
|
118
|
+
STATE_LINE="This project is at the brainstorm stage: no PRD yet, no stories. The source of truth is \`companies/$COMPANY/projects/$PROJECT/brainstorm.md\`."
|
|
119
|
+
NEXT_STEPS="1. **Read the brainstorm** — \`brainstorm.md\`, the notes under \`research/\`, and the latest journal entry under \`journal/\`.
|
|
120
|
+
2. **Turn it into a PRD** — run \`/plan $PROJECT\` to write \`prd.json\` with stories.
|
|
121
|
+
3. **Start the first story** — once the PRD exists, \`/run-project $PROJECT\` or \`/execute-task\` picks it up."
|
|
122
|
+
DOSSIER_LINE="Start with the project dossier: \`companies/$COMPANY/projects/$PROJECT/\` contains \`brainstorm.md\`, the \`research/\` notes, the latest journal entry, a full brief (the DM's details pane), and the delegation journal. There is no \`prd.json\` yet."
|
|
123
|
+
elif [ -n "$PRD_ABS" ] && [ -f "$PRD_ABS" ] && jq -e . "$PRD_ABS" >/dev/null 2>&1; then
|
|
109
124
|
GOAL="$(jq -r '.metadata.goal // .description // ""' "$PRD_ABS")"
|
|
110
125
|
TOTAL="$(jq -r '[.userStories // [] | .[]] | length' "$PRD_ABS")"
|
|
111
126
|
DONE="$(jq -r '[.userStories // [] | .[] | select(.passes == true)] | length' "$PRD_ABS")"
|
|
@@ -195,7 +210,7 @@ trap 'rm -rf "$STAGE"; [ "$SEND" -eq 0 ] || rmdir "$LOCK"' EXIT
|
|
|
195
210
|
printf '%s\n' "$GET_LINES"
|
|
196
211
|
echo '```'
|
|
197
212
|
echo
|
|
198
|
-
echo "
|
|
213
|
+
echo "$DOSSIER_LINE"
|
|
199
214
|
echo
|
|
200
215
|
if [ -n "$REPO_SECTION" ]; then
|
|
201
216
|
printf '%s\n' "$REPO_SECTION"
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Behavior (mode "transfer"; mode "share" skips every mutation):
|
|
10
10
|
# 1. companies/<co>/board.json — the project's entry gets owner=<recipient>
|
|
11
|
-
# and a bumped updated_at; created (once) if absent
|
|
12
|
-
#
|
|
11
|
+
# and a bumped updated_at; created (once) if absent. A brainstorm-stage
|
|
12
|
+
# entry (status exploring, brainstorm_path set) is matched by
|
|
13
|
+
# brainstorm_path and keeps its status.
|
|
14
|
+
# 2. prd.json metadata gains owner, delegatedFrom, delegatedAt; for a
|
|
15
|
+
# brainstorm-stage project the same three keys land in the
|
|
16
|
+
# brainstorm.md frontmatter (owner, delegated_from, delegated_at)
|
|
13
17
|
# 3. work-mesh: `done` event closes the delegator's in-progress thread and
|
|
14
18
|
# broadcasts the reassignment; silently skipped when the helper is
|
|
15
19
|
# unavailable (local/offline installs) — the transfer still succeeds
|
|
@@ -32,6 +36,9 @@ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
|
32
36
|
usage() { sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
|
33
37
|
die() { echo "hq-delegate-transfer: $*" >&2; exit 1; }
|
|
34
38
|
|
|
39
|
+
# shellcheck source=lib/delegate-brainstorm.sh
|
|
40
|
+
. "$SCRIPT_DIR/lib/delegate-brainstorm.sh"
|
|
41
|
+
|
|
35
42
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
36
43
|
|
|
37
44
|
MANIFEST=""
|
|
@@ -68,12 +75,20 @@ jq -e '.status == "granted"' "$MANIFEST" >/dev/null || die "transfer requires co
|
|
|
68
75
|
NOW="$(jq -r '.ownershipTransferredAt // empty' "$MANIFEST")"
|
|
69
76
|
[ -n "$NOW" ] || NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
70
77
|
TODAY="$(date -u +%Y-%m-%d)"
|
|
71
|
-
|
|
78
|
+
STAGE="$(jq -r '.project.stage // "prd"' "$MANIFEST")"
|
|
79
|
+
PROJECT_REL="companies/$COMPANY/projects/$PROJECT"
|
|
80
|
+
PRD_PATH="$PROJECT_REL/prd.json"
|
|
72
81
|
PRD_ABS="$HQ_ROOT/$PRD_PATH"
|
|
73
|
-
|
|
82
|
+
BRAINSTORM_PATH="$PROJECT_REL/brainstorm.md"
|
|
83
|
+
BRAINSTORM_ABS="$HQ_ROOT/$BRAINSTORM_PATH"
|
|
84
|
+
PROJECT_DIR="$HQ_ROOT/$PROJECT_REL"
|
|
74
85
|
BOARD="$HQ_ROOT/companies/$COMPANY/board.json"
|
|
75
86
|
|
|
76
|
-
|
|
87
|
+
case "$STAGE" in
|
|
88
|
+
prd) [ -f "$PRD_ABS" ] || die "prd not found: $PRD_PATH" ;;
|
|
89
|
+
brainstorm) [ -f "$BRAINSTORM_ABS" ] || die "brainstorm.md not found: $BRAINSTORM_PATH" ;;
|
|
90
|
+
*) die "manifest project.stage is '$STAGE' — expected prd or brainstorm" ;;
|
|
91
|
+
esac
|
|
77
92
|
|
|
78
93
|
# --- 1. board entry: update in place, create once if absent ------------------
|
|
79
94
|
|
|
@@ -83,47 +98,72 @@ else
|
|
|
83
98
|
echo '{"projects": []}' > "$BOARD"
|
|
84
99
|
fi
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
if [ "$STAGE" = prd ]; then
|
|
102
|
+
TITLE="$(jq -r '.name // empty' "$PRD_ABS")"
|
|
103
|
+
DESC="$(jq -r '.description // ""' "$PRD_ABS" | head -c 300)"
|
|
104
|
+
MATCH_PRD="$PRD_PATH"
|
|
105
|
+
MATCH_BS=""
|
|
106
|
+
NEW_STATUS="in_progress"
|
|
107
|
+
else
|
|
108
|
+
TITLE="$(hq_brainstorm_title "$BRAINSTORM_ABS")"
|
|
109
|
+
DESC="$(hq_brainstorm_summary "$BRAINSTORM_ABS" | head -c 300)"
|
|
110
|
+
MATCH_PRD=""
|
|
111
|
+
MATCH_BS="$BRAINSTORM_PATH"
|
|
112
|
+
NEW_STATUS="exploring"
|
|
113
|
+
fi
|
|
114
|
+
[ -n "$TITLE" ] || TITLE="$PROJECT"
|
|
88
115
|
|
|
116
|
+
# An existing entry is matched by prd_path (PRD stage) or brainstorm_path
|
|
117
|
+
# (brainstorm stage). The match is by path, so a brainstorm entry whose
|
|
118
|
+
# prd_path is null still updates in place and keeps its status.
|
|
89
119
|
TMP_BOARD="$(mktemp)"
|
|
90
120
|
jq \
|
|
91
|
-
--arg prd "$
|
|
121
|
+
--arg prd "$MATCH_PRD" \
|
|
122
|
+
--arg bs "$MATCH_BS" \
|
|
92
123
|
--arg owner "$PRINCIPAL" \
|
|
93
124
|
--arg now "$NOW" \
|
|
94
125
|
--arg title "$TITLE" \
|
|
95
126
|
--arg desc "$DESC" \
|
|
96
127
|
--arg project "$PROJECT" \
|
|
128
|
+
--arg status "$NEW_STATUS" \
|
|
97
129
|
'
|
|
98
|
-
|
|
130
|
+
def matches: ($prd != "" and .prd_path == $prd) or ($bs != "" and .brainstorm_path == $bs);
|
|
131
|
+
if ([.projects // [] | .[] | select(matches)] | length) > 0 then
|
|
99
132
|
.projects = [.projects[] |
|
|
100
|
-
if
|
|
133
|
+
if matches then .owner = $owner | .updated_at = $now else . end]
|
|
101
134
|
else
|
|
102
135
|
.projects = (.projects // []) + [{
|
|
103
136
|
id: ("proj-" + $project),
|
|
104
137
|
title: $title,
|
|
105
138
|
description: $desc,
|
|
106
|
-
status:
|
|
139
|
+
status: $status,
|
|
107
140
|
owner: $owner,
|
|
108
|
-
prd_path: $prd,
|
|
141
|
+
prd_path: (if $prd == "" then null else $prd end),
|
|
109
142
|
created_at: $now,
|
|
110
143
|
updated_at: $now
|
|
111
|
-
}]
|
|
144
|
+
} + (if $bs == "" then {} else {brainstorm_path: $bs} end)]
|
|
112
145
|
end
|
|
113
146
|
| .updated_at = $now
|
|
114
147
|
' "$BOARD" > "$TMP_BOARD" && mv "$TMP_BOARD" "$BOARD"
|
|
115
148
|
|
|
116
|
-
# --- 2.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
# --- 2. ownership on the source of truth -------------------------------------
|
|
150
|
+
# PRD stage: prd.json metadata. Brainstorm stage: brainstorm.md frontmatter.
|
|
151
|
+
|
|
152
|
+
if [ "$STAGE" = prd ]; then
|
|
153
|
+
TMP_PRD="$(mktemp)"
|
|
154
|
+
jq \
|
|
155
|
+
--arg owner "$PRINCIPAL" \
|
|
156
|
+
--arg from "$FROM" \
|
|
157
|
+
--arg now "$NOW" \
|
|
158
|
+
'.metadata.owner = $owner
|
|
159
|
+
| .metadata.delegatedFrom = $from
|
|
160
|
+
| .metadata.delegatedAt = $now' \
|
|
161
|
+
"$PRD_ABS" > "$TMP_PRD" && mv "$TMP_PRD" "$PRD_ABS"
|
|
162
|
+
else
|
|
163
|
+
hq_brainstorm_frontmatter_set "$BRAINSTORM_ABS" owner "$PRINCIPAL"
|
|
164
|
+
hq_brainstorm_frontmatter_set "$BRAINSTORM_ABS" delegated_from "$FROM"
|
|
165
|
+
hq_brainstorm_frontmatter_set "$BRAINSTORM_ABS" delegated_at "$NOW"
|
|
166
|
+
fi
|
|
127
167
|
|
|
128
168
|
# --- 3. work mesh: close the delegator's thread, broadcast reassignment ------
|
|
129
169
|
# Silently tolerated when unavailable (local/offline installs no-op).
|
|
@@ -162,7 +202,11 @@ if ! grep -qF "$DELEGATION_ID" "$JOURNAL_FILE"; then
|
|
|
162
202
|
echo "- From: $FROM"
|
|
163
203
|
echo "- To: $DISPLAY ($PRINCIPAL)"
|
|
164
204
|
echo "- Transferred scope: $SCOPE"
|
|
165
|
-
|
|
205
|
+
if [ "$STAGE" = prd ]; then
|
|
206
|
+
echo "- Local board and PRD ownership updated; Work Mesh ownership is unconfirmed. Existing delegator access is unchanged."
|
|
207
|
+
else
|
|
208
|
+
echo "- Brainstorm-stage project (no PRD yet). Local board and brainstorm.md ownership updated; Work Mesh ownership is unconfirmed. Existing delegator access is unchanged."
|
|
209
|
+
fi
|
|
166
210
|
} >> "$JOURNAL_FILE"
|
|
167
211
|
fi
|
|
168
212
|
|
|
@@ -172,4 +216,8 @@ TMP_MANIFEST="$(mktemp)"
|
|
|
172
216
|
jq --arg now "$NOW" '.ownershipTransferredAt = $now | .workMeshOwnership = "unconfirmed"' "$MANIFEST" > "$TMP_MANIFEST" \
|
|
173
217
|
&& mv "$TMP_MANIFEST" "$MANIFEST"
|
|
174
218
|
|
|
175
|
-
|
|
219
|
+
if [ "$STAGE" = prd ]; then
|
|
220
|
+
echo "hq-delegate-transfer: ownership of '$PROJECT' transferred to $DISPLAY ($PRINCIPAL) — local board, PRD, and journal updated; Work Mesh ownership unconfirmed"
|
|
221
|
+
else
|
|
222
|
+
echo "hq-delegate-transfer: ownership of '$PROJECT' (brainstorm stage) transferred to $DISPLAY ($PRINCIPAL) — local board, brainstorm.md, and journal updated; Work Mesh ownership unconfirmed"
|
|
223
|
+
fi
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
# core/scripts/hq-delegate-verify.sh --dry-run \
|
|
18
18
|
# --company <slug> --project <name> --to <principal> [--mode transfer|share]
|
|
19
19
|
#
|
|
20
|
-
# Prints recipient, mode,
|
|
20
|
+
# Prints recipient, mode, project stage (prd, or brainstorm with the dossier
|
|
21
|
+
# files and the /plan next step), every prefix with its permission, the secret
|
|
21
22
|
# names an .env.schema would grant, repo + branch, and the DM headline that
|
|
22
23
|
# would be sent. Exits 0. Creates nothing under workspace/delegations/ and
|
|
23
24
|
# invokes no mutating command.
|
|
@@ -83,6 +84,14 @@ if [ "$DRY_RUN" -eq 1 ]; then
|
|
|
83
84
|
echo " Mode: $MODE"
|
|
84
85
|
echo " Company: $COMPANY"
|
|
85
86
|
echo " Project: $PROJECT"
|
|
87
|
+
if [ "$(printf '%s' "$PLAN_JSON" | jq -r '.project.stage // "prd"')" = brainstorm ]; then
|
|
88
|
+
echo " Stage: brainstorm — no PRD yet; source of truth is brainstorm.md; recommended next step is /plan $PROJECT"
|
|
89
|
+
echo
|
|
90
|
+
echo " Dossier files that would be published:"
|
|
91
|
+
printf '%s' "$PLAN_JSON" | jq -r '.project.dossier[] | " \(.)"'
|
|
92
|
+
else
|
|
93
|
+
echo " Stage: prd"
|
|
94
|
+
fi
|
|
86
95
|
echo
|
|
87
96
|
echo " Vault grants that would be written:"
|
|
88
97
|
printf '%s' "$PLAN_JSON" | jq -r '.vaultPrefixes[] | " \(.permission)\ton \(.prefix)\t(\(.reason // ""))"'
|
|
@@ -123,12 +123,19 @@ command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
|
123
123
|
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
124
124
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
125
125
|
msys*:*|*:MINGW*)
|
|
126
|
+
# Probe --binary once per script run, in the top-level shell. Most jq
|
|
127
|
+
# calls run inside $(...) subshells, so a cache filled lazily inside jq()
|
|
128
|
+
# would be lost and the probe would repeat on every call.
|
|
129
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then
|
|
130
|
+
HQ_JQ_BINARY_SUPPORTED=1
|
|
131
|
+
else
|
|
132
|
+
HQ_JQ_BINARY_SUPPORTED=0
|
|
133
|
+
fi
|
|
126
134
|
jq() {
|
|
127
|
-
local stdout_file stderr_file status
|
|
135
|
+
local stdout_file stderr_file status
|
|
128
136
|
stdout_file="$(mktemp)" || return $?
|
|
129
137
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
130
|
-
if
|
|
131
|
-
if [[ "$binary_supported" == 1 ]]; then
|
|
138
|
+
if [[ "$HQ_JQ_BINARY_SUPPORTED" == 1 ]]; then
|
|
132
139
|
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
133
140
|
command cat "$stdout_file"
|
|
134
141
|
command cat "$stderr_file" >&2
|
|
@@ -43,12 +43,19 @@ command -v jq >/dev/null 2>&1 || {
|
|
|
43
43
|
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
44
44
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
45
45
|
msys*:*|*:MINGW*)
|
|
46
|
+
# Probe --binary once per script run, in the top-level shell. Most jq
|
|
47
|
+
# calls run inside $(...) subshells, so a cache filled lazily inside jq()
|
|
48
|
+
# would be lost and the probe would repeat on every call.
|
|
49
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then
|
|
50
|
+
HQ_JQ_BINARY_SUPPORTED=1
|
|
51
|
+
else
|
|
52
|
+
HQ_JQ_BINARY_SUPPORTED=0
|
|
53
|
+
fi
|
|
46
54
|
jq() {
|
|
47
|
-
local stdout_file stderr_file status
|
|
55
|
+
local stdout_file stderr_file status
|
|
48
56
|
stdout_file="$(mktemp)" || return $?
|
|
49
57
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
50
|
-
if
|
|
51
|
-
if [[ "$binary_supported" == 1 ]]; then
|
|
58
|
+
if [[ "$HQ_JQ_BINARY_SUPPORTED" == 1 ]]; then
|
|
52
59
|
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
53
60
|
command cat "$stdout_file"
|
|
54
61
|
command cat "$stderr_file" >&2
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# delegate-brainstorm.sh — shared readers for brainstorm-stage projects in the
|
|
4
|
+
# /delegate helpers.
|
|
5
|
+
#
|
|
6
|
+
# SOURCED, never executed: . "$ROOT/core/scripts/lib/delegate-brainstorm.sh"
|
|
7
|
+
# bash 3.2 safe; no associative arrays, no GNU-only flags.
|
|
8
|
+
#
|
|
9
|
+
# A brainstorm-stage project has companies/<co>/projects/<slug>/brainstorm.md
|
|
10
|
+
# and no prd.json yet (policy hq-brainstorm-leaves-handoffable-project-folder).
|
|
11
|
+
# Every delegation helper that reads the PRD falls back to these readers when
|
|
12
|
+
# the manifest says project.stage == "brainstorm".
|
|
13
|
+
|
|
14
|
+
# Project stage from the files on disk. Prints "prd", "brainstorm", or "none".
|
|
15
|
+
hq_delegate_project_stage() { # project_dir
|
|
16
|
+
if [ -f "$1/prd.json" ]; then
|
|
17
|
+
echo prd
|
|
18
|
+
elif [ -f "$1/brainstorm.md" ]; then
|
|
19
|
+
echo brainstorm
|
|
20
|
+
else
|
|
21
|
+
echo none
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Value of a top-level `key: value` line inside the leading YAML frontmatter.
|
|
26
|
+
# Prints nothing when the file has no frontmatter or the key is absent.
|
|
27
|
+
hq_brainstorm_frontmatter_get() { # file key
|
|
28
|
+
awk -v key="$2" '
|
|
29
|
+
NR == 1 { if ($0 != "---") exit; next }
|
|
30
|
+
/^---[[:space:]]*$/ { exit }
|
|
31
|
+
{
|
|
32
|
+
split($0, kv, ":")
|
|
33
|
+
if (kv[1] == key) {
|
|
34
|
+
sub("^[^:]*:[[:space:]]*", "", $0)
|
|
35
|
+
gsub(/^"|"$/, "", $0)
|
|
36
|
+
print
|
|
37
|
+
exit
|
|
38
|
+
}
|
|
39
|
+
}' "$1"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# One-line summary: the first `> ` blockquote after the H1, else the first
|
|
43
|
+
# non-empty prose line after the H1.
|
|
44
|
+
hq_brainstorm_summary() { # file
|
|
45
|
+
awk '
|
|
46
|
+
/^# / && !seen_h1 { seen_h1 = 1; next }
|
|
47
|
+
!seen_h1 { next }
|
|
48
|
+
/^> / { sub(/^> /, ""); print; exit }
|
|
49
|
+
/^#/ { exit }
|
|
50
|
+
/^[[:space:]]*$/ { next }
|
|
51
|
+
{ print; exit }' "$1"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Project title: the first H1 without the leading `# `.
|
|
55
|
+
hq_brainstorm_title() { # file
|
|
56
|
+
awk '/^# / { sub(/^# /, ""); print; exit }' "$1"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Body of a `## <heading>` section, up to the next `## ` heading. The heading
|
|
60
|
+
# is matched by prefix so `## Recommendation` also matches a decorated title.
|
|
61
|
+
hq_brainstorm_section() { # file heading
|
|
62
|
+
awk -v heading="$2" '
|
|
63
|
+
/^## / {
|
|
64
|
+
if (in_section) exit
|
|
65
|
+
if (index($0, "## " heading) == 1) { in_section = 1; next }
|
|
66
|
+
}
|
|
67
|
+
in_section { print }' "$1" | sed -e '/./,$!d' | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Newest journal note by filename, excluding the delegation log the transfer
|
|
71
|
+
# step maintains. Prints the HQ-root-relative path or nothing.
|
|
72
|
+
hq_brainstorm_latest_journal() { # hq_root rel_project_dir
|
|
73
|
+
local dir="$1/$2/journal" f newest="" name newest_name=""
|
|
74
|
+
[ -d "$dir" ] || return 0
|
|
75
|
+
# Bash's pathname expansion is sorted by locale like sort(1), and unlike
|
|
76
|
+
# find -maxdepth it works with both GNU and macOS/BSD find installations.
|
|
77
|
+
# Include dotfiles too: find includes them, while Bash's default glob does not.
|
|
78
|
+
for f in "$dir"/*.md "$dir"/.md "$dir"/.[!.]*.md "$dir"/..?*.md; do
|
|
79
|
+
[ -f "$f" ] || continue
|
|
80
|
+
name="${f##*/}"
|
|
81
|
+
[ "$name" = delegations.md ] && continue
|
|
82
|
+
case "$name" in *.md) ;; *) continue ;; esac
|
|
83
|
+
if [ -z "$newest" ]; then newest="$f"; newest_name="$name"; continue; fi
|
|
84
|
+
if [[ "$name" > "$newest_name" ]]; then newest="$f"; newest_name="$name"; fi
|
|
85
|
+
done
|
|
86
|
+
[ -n "$newest" ] || return 0
|
|
87
|
+
printf '%s\n' "${newest#"$1/"}"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# HQ-root-relative dossier paths for a brainstorm-stage project, one per line:
|
|
91
|
+
# brainstorm.md, every file under research/, the newest journal note.
|
|
92
|
+
hq_brainstorm_dossier() { # hq_root rel_project_dir
|
|
93
|
+
local root="$1" rel="$2"
|
|
94
|
+
printf '%s\n' "$rel/brainstorm.md"
|
|
95
|
+
if [ -d "$root/$rel/research" ]; then
|
|
96
|
+
(cd "$root" && find "$rel/research" -type f ! -name '.DS_Store' | sort)
|
|
97
|
+
fi
|
|
98
|
+
hq_brainstorm_latest_journal "$root" "$rel"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Write or replace `key: value` inside the frontmatter, creating the
|
|
102
|
+
# frontmatter block when the file has none. Rewrites the file in place through
|
|
103
|
+
# a temp file so a failure leaves the original intact.
|
|
104
|
+
hq_brainstorm_frontmatter_set() { # file key value
|
|
105
|
+
local file="$1" key="$2" value="$3" tmp
|
|
106
|
+
tmp="$(mktemp "${file}.tmp.XXXXXX")"
|
|
107
|
+
if [ "$(head -1 "$file")" = "---" ]; then
|
|
108
|
+
awk -v key="$key" -v value="$value" '
|
|
109
|
+
NR == 1 { print; next }
|
|
110
|
+
!done && /^---[[:space:]]*$/ { print key ": " value; done = 1; print; next }
|
|
111
|
+
!done {
|
|
112
|
+
split($0, kv, ":")
|
|
113
|
+
if (kv[1] == key) { print key ": " value; done = 1; next }
|
|
114
|
+
}
|
|
115
|
+
{ print }' "$file" > "$tmp"
|
|
116
|
+
else
|
|
117
|
+
{ printf -- '---\n%s: %s\n---\n' "$key" "$value"; cat "$file"; } > "$tmp"
|
|
118
|
+
fi
|
|
119
|
+
mv -f "$tmp" "$file"
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type BrainstormStage = "prd" | "brainstorm" | "none";
|
|
2
|
+
export declare function projectStage(projectDir: string): BrainstormStage;
|
|
3
|
+
export declare function brainstormFrontmatterGet(file: string, key: string): string;
|
|
4
|
+
export declare function brainstormTitle(file: string): string;
|
|
5
|
+
export declare function brainstormSummary(file: string): string;
|
|
6
|
+
export declare function brainstormSection(file: string, heading: string): string;
|
|
7
|
+
export declare function brainstormDossier(root: string, relativeProject: string): string[];
|
|
8
|
+
export declare function brainstormFrontmatterSet(file: string, key: string, value: string): void;
|
|
9
|
+
//# sourceMappingURL=core-delegate-brainstorm.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
export function projectStage(projectDir) {
|
|
5
|
+
if (fs.existsSync(path.join(projectDir, "prd.json")))
|
|
6
|
+
return "prd";
|
|
7
|
+
if (fs.existsSync(path.join(projectDir, "brainstorm.md")))
|
|
8
|
+
return "brainstorm";
|
|
9
|
+
return "none";
|
|
10
|
+
}
|
|
11
|
+
export function brainstormFrontmatterGet(file, key) {
|
|
12
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
13
|
+
if (lines[0] !== "---")
|
|
14
|
+
return "";
|
|
15
|
+
for (const line of lines.slice(1)) {
|
|
16
|
+
if (/^---[\t ]*$/.test(line))
|
|
17
|
+
break;
|
|
18
|
+
const colon = line.indexOf(":");
|
|
19
|
+
if (colon < 0 || line.slice(0, colon) !== key)
|
|
20
|
+
continue;
|
|
21
|
+
return line.slice(colon + 1).replace(/^[\t ]*/, "").replace(/^"|"$/g, "");
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
export function brainstormTitle(file) {
|
|
26
|
+
return fs.readFileSync(file, "utf8").split("\n").find((line) => line.startsWith("# "))?.slice(2) ?? "";
|
|
27
|
+
}
|
|
28
|
+
export function brainstormSummary(file) {
|
|
29
|
+
let seenH1 = false;
|
|
30
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
31
|
+
if (!seenH1 && line.startsWith("# ")) {
|
|
32
|
+
seenH1 = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!seenH1)
|
|
36
|
+
continue;
|
|
37
|
+
if (line.startsWith("> "))
|
|
38
|
+
return line.slice(2);
|
|
39
|
+
if (line.startsWith("#"))
|
|
40
|
+
return "";
|
|
41
|
+
if (!line.trim())
|
|
42
|
+
continue;
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
export function brainstormSection(file, heading) {
|
|
48
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
49
|
+
const section = [];
|
|
50
|
+
let inSection = false;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (line.startsWith("## ")) {
|
|
53
|
+
if (inSection)
|
|
54
|
+
break;
|
|
55
|
+
if (line.startsWith(`## ${heading}`)) {
|
|
56
|
+
inSection = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (inSection)
|
|
61
|
+
section.push(line);
|
|
62
|
+
}
|
|
63
|
+
while (section[0] === "")
|
|
64
|
+
section.shift();
|
|
65
|
+
while (section.at(-1) === "")
|
|
66
|
+
section.pop();
|
|
67
|
+
return section.join("\n");
|
|
68
|
+
}
|
|
69
|
+
function filesBelow(directory) {
|
|
70
|
+
if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory())
|
|
71
|
+
return [];
|
|
72
|
+
const files = [];
|
|
73
|
+
const visit = (current) => {
|
|
74
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
75
|
+
const target = path.join(current, entry.name);
|
|
76
|
+
if (entry.isDirectory())
|
|
77
|
+
visit(target);
|
|
78
|
+
else if (entry.isFile() && entry.name !== ".DS_Store")
|
|
79
|
+
files.push(target);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
visit(directory);
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
export function brainstormDossier(root, relativeProject) {
|
|
86
|
+
const projectDir = path.join(root, relativeProject);
|
|
87
|
+
const result = [`${relativeProject}/brainstorm.md`];
|
|
88
|
+
result.push(...filesBelow(path.join(projectDir, "research"))
|
|
89
|
+
.map((file) => path.relative(root, file).split(path.sep).join("/")).sort());
|
|
90
|
+
const journal = path.join(projectDir, "journal");
|
|
91
|
+
if (fs.existsSync(journal) && fs.statSync(journal).isDirectory()) {
|
|
92
|
+
const newest = fs.readdirSync(journal)
|
|
93
|
+
.filter((name) => name.endsWith(".md") && name !== "delegations.md" && isRegularFile(path.join(journal, name)))
|
|
94
|
+
.sort().at(-1);
|
|
95
|
+
if (newest)
|
|
96
|
+
result.push(`${relativeProject}/journal/${newest}`);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function isRegularFile(file) {
|
|
101
|
+
try {
|
|
102
|
+
return fs.statSync(file).isFile();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function brainstormFrontmatterSet(file, key, value) {
|
|
109
|
+
const contents = fs.readFileSync(file, "utf8");
|
|
110
|
+
const lines = contents.split("\n");
|
|
111
|
+
let output;
|
|
112
|
+
if (lines[0] === "---") {
|
|
113
|
+
output = [];
|
|
114
|
+
let done = false;
|
|
115
|
+
let closed = false;
|
|
116
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
117
|
+
const line = lines[index] ?? "";
|
|
118
|
+
if (index === 0) {
|
|
119
|
+
output.push(line);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (/^---[\t ]*$/.test(line)) {
|
|
123
|
+
if (!done) {
|
|
124
|
+
output.push(`${key}: ${value}`);
|
|
125
|
+
done = true;
|
|
126
|
+
}
|
|
127
|
+
output.push(line);
|
|
128
|
+
output.push(...lines.slice(index + 1));
|
|
129
|
+
closed = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const colon = line.indexOf(":");
|
|
133
|
+
if (!done && colon >= 0 && line.slice(0, colon) === key) {
|
|
134
|
+
output.push(`${key}: ${value}`);
|
|
135
|
+
done = true;
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
output.push(line);
|
|
139
|
+
}
|
|
140
|
+
if (!closed)
|
|
141
|
+
output = output.length ? output : lines;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
output = ["---", `${key}: ${value}`, "---", ...lines];
|
|
145
|
+
}
|
|
146
|
+
const temporary = `${file}.tmp.${randomBytes(6).toString("hex")}`;
|
|
147
|
+
const rewritten = output.join("\n");
|
|
148
|
+
const contentsToWrite = lines[0] === "---" && !rewritten.endsWith("\n") ? `${rewritten}\n` : rewritten;
|
|
149
|
+
fs.writeFileSync(temporary, contentsToWrite, { mode: 0o600, flag: "wx" });
|
|
150
|
+
try {
|
|
151
|
+
fs.renameSync(temporary, file);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(temporary, { force: true });
|
|
156
|
+
}
|
|
157
|
+
catch { /* retain primary failure */ }
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=core-delegate-brainstorm.js.map
|