@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.
@@ -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
- PRD_ABS="$HQ_ROOT/$(jq -r '.project.prdPath' "$MANIFEST")"
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
- if [ -f "$PRD_ABS" ] && jq -e . "$PRD_ABS" >/dev/null 2>&1; then
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 "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."
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
- # 2. prd.json metadata gains owner, delegatedFrom, delegatedAt
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
- PRD_PATH="companies/$COMPANY/projects/$PROJECT/prd.json"
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
- PROJECT_DIR="$HQ_ROOT/companies/$COMPANY/projects/$PROJECT"
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
- [ -f "$PRD_ABS" ] || die "prd not found: $PRD_PATH"
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
- TITLE="$(jq -r '.name // empty' "$PRD_ABS")"
87
- DESC="$(jq -r '.description // ""' "$PRD_ABS" | head -c 300)"
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 "$PRD_PATH" \
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
- if ([.projects // [] | .[] | select(.prd_path == $prd)] | length) > 0 then
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 .prd_path == $prd then .owner = $owner | .updated_at = $now else . end]
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: "in_progress",
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. prd metadata ----------------------------------------------------------
117
-
118
- TMP_PRD="$(mktemp)"
119
- jq \
120
- --arg owner "$PRINCIPAL" \
121
- --arg from "$FROM" \
122
- --arg now "$NOW" \
123
- '.metadata.owner = $owner
124
- | .metadata.delegatedFrom = $from
125
- | .metadata.delegatedAt = $now' \
126
- "$PRD_ABS" > "$TMP_PRD" && mv "$TMP_PRD" "$PRD_ABS"
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
- echo "- Local board and PRD ownership updated; Work Mesh ownership is unconfirmed. Existing delegator access is unchanged."
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
- echo "hq-delegate-transfer: ownership of '$PROJECT' transferred to $DISPLAY ($PRINCIPAL) — local board, PRD, and journal updated; Work Mesh ownership unconfirmed"
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, every prefix with its permission, the secret
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 binary_supported
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 command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
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 binary_supported
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 command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
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