@raquezha/norpiv 0.0.2

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.
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Triage Helper: Manages namespaced task workspaces.
4
+ # Usage: ./triage_helper.sh [github|gitlab|jira|local] [id] [auto|resume|reopen|fresh|reset]
5
+ #
6
+ # Default mode is auto:
7
+ # - create missing tasks
8
+ # - resume existing active/blocked tasks
9
+ # - refuse done/archived tasks unless explicitly reopened/reset
10
+
11
+ set -euo pipefail
12
+
13
+ SOURCE=${1:-}
14
+ ID=${2:-}
15
+ MODE=${3:-auto}
16
+ BASE_DIR=".workflow/tasks"
17
+
18
+ if [[ -z "$SOURCE" || -z "$ID" ]]; then
19
+ echo "Usage: $0 [github|gitlab|jira|local] [id] [auto|resume|reopen|fresh|reset]"
20
+ exit 1
21
+ fi
22
+
23
+ case "$MODE" in
24
+ auto|resume|reopen|fresh|reset) ;;
25
+ *)
26
+ echo "Unknown mode: $MODE"
27
+ echo "Valid modes: auto, resume, reopen, fresh, reset"
28
+ exit 1
29
+ ;;
30
+ esac
31
+
32
+ if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
33
+ echo "ERROR: triage_helper.sh must be run inside a git repository."
34
+ exit 1
35
+ fi
36
+
37
+ REPO_ROOT=$(git rev-parse --show-toplevel)
38
+ cd "$REPO_ROOT"
39
+
40
+ BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
41
+ NOW=$(LC_TIME=C date +"%Y-%m-%d %I:%M %p")
42
+ ISO_NOW=$(LC_TIME=C date -u +"%Y-%m-%dT%H:%M:%SZ")
43
+
44
+ # Sanitize local ID if generic (case-insensitive check)
45
+ ID_LOWER=$(echo "$ID" | tr '[:upper:]' '[:lower:]')
46
+ if [[ "$SOURCE" == "local" && "$ID_LOWER" =~ ^(problem|task|issue|work|todo)$ ]]; then
47
+ ID=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9]/-/g')
48
+ echo "Generic local ID detected. Falling back to branch-derived name '$ID'..."
49
+ fi
50
+
51
+ TASK_FOLDER="$SOURCE-$ID"
52
+ TASK_DIR="$BASE_DIR/$TASK_FOLDER"
53
+ WORK_MD="$TASK_DIR/WORK.md"
54
+ METADATA_JSON="$TASK_DIR/metadata.json"
55
+
56
+ json_upsert() {
57
+ local file="$1"
58
+ shift
59
+ python3 - "$file" "$@" <<'PY'
60
+ import json
61
+ import sys
62
+ from pathlib import Path
63
+
64
+ path = Path(sys.argv[1])
65
+ updates = {}
66
+ preserve_existing = set()
67
+ for item in sys.argv[2:]:
68
+ key, value = item.split("=", 1)
69
+ if key.startswith("?"):
70
+ key = key[1:]
71
+ preserve_existing.add(key)
72
+ updates[key] = value
73
+
74
+ if path.exists():
75
+ raw = path.read_text().strip()
76
+ if raw:
77
+ try:
78
+ data = json.loads(raw)
79
+ except json.JSONDecodeError:
80
+ data = {"raw": raw}
81
+ else:
82
+ data = {}
83
+ else:
84
+ data = {}
85
+
86
+ if not isinstance(data, dict):
87
+ data = {"raw": data}
88
+
89
+ for key, value in updates.items():
90
+ if key == "createdAt" and data.get("createdAt"):
91
+ continue
92
+ if key in preserve_existing and data.get(key):
93
+ continue
94
+ data[key] = value
95
+
96
+ path.parent.mkdir(parents=True, exist_ok=True)
97
+ path.write_text(json.dumps(data, indent=2, sort_keys=False) + "\n")
98
+ PY
99
+ }
100
+
101
+ json_get() {
102
+ local file="$1"
103
+ local key="$2"
104
+ python3 - "$file" "$key" <<'PY'
105
+ import json
106
+ import sys
107
+ from pathlib import Path
108
+
109
+ path = Path(sys.argv[1])
110
+ key = sys.argv[2]
111
+ try:
112
+ data = json.loads(path.read_text())
113
+ if isinstance(data, dict):
114
+ value = data.get(key, "")
115
+ print("" if value is None else value)
116
+ except Exception:
117
+ print("")
118
+ PY
119
+ }
120
+
121
+ write_active_pointer() {
122
+ mkdir -p ".workflow"
123
+ python3 - ".workflow/active_task.json" "$TASK_FOLDER" "$SOURCE" "$ID" "$TASK_DIR" "$BRANCH_NAME" <<'PY'
124
+ import json
125
+ import sys
126
+ from pathlib import Path
127
+
128
+ path, active_task, source, raw_id, task_path, branch = sys.argv[1:]
129
+ data = {
130
+ "active_task": active_task,
131
+ "source": source,
132
+ "id": raw_id,
133
+ "sourceId": raw_id,
134
+ "taskPath": task_path,
135
+ "path": task_path,
136
+ "branch": branch,
137
+ }
138
+ Path(path).write_text(json.dumps(data, indent=2) + "\n")
139
+ PY
140
+ }
141
+
142
+ ensure_section() {
143
+ local section="$1"
144
+ local body="$2"
145
+
146
+ if [[ ! -f "$WORK_MD" ]]; then
147
+ return
148
+ fi
149
+
150
+ if ! grep -Eq "^(## )?\[$section\][[:space:]]*$" "$WORK_MD"; then
151
+ printf '\n## [%s]\n%s\n' "$section" "$body" >> "$WORK_MD"
152
+ fi
153
+ }
154
+
155
+ append_log() {
156
+ local message="$1"
157
+ local entry="- $NOW: $message"
158
+ ensure_section "LOG" ""
159
+ python3 - "$WORK_MD" "$entry" <<'PY'
160
+ import re
161
+ import sys
162
+ from pathlib import Path
163
+
164
+ path = Path(sys.argv[1])
165
+ entry = sys.argv[2]
166
+ text = path.read_text() if path.exists() else ""
167
+ lines = text.splitlines()
168
+ log_re = re.compile(r"^(## )?\[LOG\]\s*$")
169
+ header_re = re.compile(r"^(## )?\[[A-Z0-9_-]+\]\s*$")
170
+ start = next((i for i, line in enumerate(lines) if log_re.match(line)), None)
171
+ if start is None:
172
+ if text and not text.endswith("\n"):
173
+ text += "\n"
174
+ text += "\n## [LOG]\n" + entry + "\n"
175
+ path.write_text(text)
176
+ raise SystemExit
177
+ end = len(lines)
178
+ for i in range(start + 1, len(lines)):
179
+ if header_re.match(lines[i]):
180
+ end = i
181
+ break
182
+ while end > start + 1 and lines[end - 1].strip() == "":
183
+ end -= 1
184
+ updated = lines[:end] + [entry] + lines[end:]
185
+ path.write_text("\n".join(updated).rstrip() + "\n")
186
+ PY
187
+ }
188
+
189
+ update_work_meta() {
190
+ local status phase
191
+ status=$(json_get "$METADATA_JSON" status)
192
+ phase=$(json_get "$METADATA_JSON" phase)
193
+ status=${status:-active}
194
+ phase=${phase:-triaged}
195
+
196
+ python3 - "$WORK_MD" "$BRANCH_NAME" "$status" "$phase" "$SOURCE" "$ID" <<'PY'
197
+ import re
198
+ import sys
199
+ from pathlib import Path
200
+
201
+ path = Path(sys.argv[1])
202
+ branch, status, phase, source, raw_id = sys.argv[2:]
203
+ text = path.read_text() if path.exists() else ""
204
+ lines = text.splitlines()
205
+ header_re = re.compile(r"^(## )?\[[A-Z0-9_-]+\]\s*$")
206
+ meta_re = re.compile(r"^(## )?\[META\]\s*$")
207
+
208
+ new_block = [
209
+ "## [META]",
210
+ f"- Branch: `{branch}`",
211
+ f"- Status: `{status}`",
212
+ f"- Phase: `{phase}`",
213
+ f"- Source: `{source}:{raw_id}`",
214
+ ]
215
+
216
+ start = next((i for i, line in enumerate(lines) if meta_re.match(line)), None)
217
+ if start is None:
218
+ if text and not text.endswith("\n"):
219
+ text += "\n"
220
+ text += "\n" + "\n".join(new_block) + "\n"
221
+ path.write_text(text)
222
+ sys.exit(0)
223
+
224
+ end = len(lines)
225
+ for i in range(start + 1, len(lines)):
226
+ if header_re.match(lines[i]):
227
+ end = i
228
+ break
229
+
230
+ updated = lines[:start] + new_block + lines[end:]
231
+ path.write_text("\n".join(updated).rstrip() + "\n")
232
+ PY
233
+ }
234
+
235
+ backfill_metadata() {
236
+ json_upsert "$METADATA_JSON" \
237
+ "id=$ID" \
238
+ "source=$SOURCE" \
239
+ "branch=$BRANCH_NAME" \
240
+ "taskFolder=$TASK_FOLDER" \
241
+ "?status=active" \
242
+ "?phase=triaged" \
243
+ "createdAt=$ISO_NOW" \
244
+ "updatedAt=$ISO_NOW"
245
+ }
246
+
247
+ set_metadata_status_phase() {
248
+ local status="$1"
249
+ local phase="$2"
250
+ json_upsert "$METADATA_JSON" \
251
+ "id=$ID" \
252
+ "source=$SOURCE" \
253
+ "branch=$BRANCH_NAME" \
254
+ "taskFolder=$TASK_FOLDER" \
255
+ "status=$status" \
256
+ "phase=$phase" \
257
+ "createdAt=$ISO_NOW" \
258
+ "updatedAt=$ISO_NOW"
259
+ }
260
+
261
+ create_task() {
262
+ mkdir -p "$TASK_DIR"
263
+ echo "Creating task workspace in $TASK_DIR..."
264
+
265
+ case "$SOURCE" in
266
+ github)
267
+ command -v gh >/dev/null 2>&1 || { echo "ERROR: gh CLI is required for github tasks."; exit 1; }
268
+ echo "Fetching GitHub Issue #$ID..."
269
+ gh issue view "$ID" --json title,body,author,labels,comments > "$METADATA_JSON"
270
+ echo "# WORK: GitHub #$ID" > "$WORK_MD"
271
+ gh issue view "$ID" >> "$WORK_MD"
272
+ ;;
273
+ gitlab)
274
+ command -v glab >/dev/null 2>&1 || { echo "ERROR: glab CLI is required for gitlab tasks."; exit 1; }
275
+ echo "Fetching GitLab Issue #$ID..."
276
+ glab issue view "$ID" > "$WORK_MD"
277
+ echo '{}' > "$METADATA_JSON"
278
+ ;;
279
+ jira)
280
+ echo "Fetching Jira Ticket $ID..."
281
+ if command -v jira >/dev/null 2>&1; then
282
+ jira issue view "$ID" > "$WORK_MD"
283
+ jira issue view "$ID" --raw > "$METADATA_JSON"
284
+ elif command -v acli >/dev/null 2>&1; then
285
+ acli jira workitem view "$ID" > "$WORK_MD"
286
+ echo '{}' > "$METADATA_JSON"
287
+ else
288
+ echo "ERROR: jira or acli CLI is required for jira tasks."
289
+ exit 1
290
+ fi
291
+ ;;
292
+ local)
293
+ echo "Initializing local task workspace: $ID..."
294
+ echo "# WORK: Local Task $ID" > "$WORK_MD"
295
+ echo '{}' > "$METADATA_JSON"
296
+ ;;
297
+ *)
298
+ echo "Unknown source: $SOURCE"
299
+ exit 1
300
+ ;;
301
+ esac
302
+
303
+ set_metadata_status_phase "active" "triaged"
304
+ ensure_section "BRIEF" "- "
305
+ ensure_section "GRILL" "- "
306
+ ensure_section "PLAN" "- [ ] "
307
+ ensure_section "LOG" ""
308
+ update_work_meta
309
+ append_log "Task initialized via /triage"
310
+ write_active_pointer
311
+ echo "Triage complete. Created WORK.md at $WORK_MD."
312
+ }
313
+
314
+ resume_task() {
315
+ if [[ ! -f "$WORK_MD" ]]; then
316
+ echo "ERROR: Cannot resume; WORK.md not found at $WORK_MD"
317
+ exit 1
318
+ fi
319
+
320
+ [[ -f "$METADATA_JSON" ]] || echo '{}' > "$METADATA_JSON"
321
+ backfill_metadata
322
+ ensure_section "BRIEF" "- "
323
+ ensure_section "GRILL" "- "
324
+ ensure_section "PLAN" "- [ ] "
325
+ ensure_section "LOG" ""
326
+ update_work_meta
327
+ append_log "Task resumed via /triage"
328
+ write_active_pointer
329
+ echo "Triage complete. Resumed existing task at $TASK_DIR."
330
+ }
331
+
332
+ reopen_task() {
333
+ if [[ ! -f "$WORK_MD" ]]; then
334
+ echo "ERROR: Cannot reopen; WORK.md not found at $WORK_MD"
335
+ exit 1
336
+ fi
337
+
338
+ set_metadata_status_phase "active" "triaged"
339
+ ensure_section "BRIEF" "- "
340
+ ensure_section "GRILL" "- "
341
+ ensure_section "PLAN" "- [ ] "
342
+ ensure_section "LOG" ""
343
+ update_work_meta
344
+ append_log "Task reopened via /triage"
345
+ write_active_pointer
346
+ echo "Triage complete. Reopened task at $TASK_DIR."
347
+ }
348
+
349
+ fresh_task() {
350
+ if [[ -e "$TASK_DIR" ]]; then
351
+ local backup_dir
352
+ backup_dir="$TASK_DIR.archive.$(LC_TIME=C date -u +"%Y%m%dT%H%M%SZ")"
353
+ mv "$TASK_DIR" "$backup_dir"
354
+ echo "Archived existing task workspace to $backup_dir"
355
+ fi
356
+ create_task
357
+ }
358
+
359
+ if [[ -f "$WORK_MD" ]]; then
360
+ [[ -f "$METADATA_JSON" ]] || echo '{}' > "$METADATA_JSON"
361
+ STATUS=$(json_get "$METADATA_JSON" status)
362
+ STATUS=${STATUS:-active}
363
+
364
+ case "$MODE" in
365
+ auto|resume)
366
+ case "$STATUS" in
367
+ active|blocked|"")
368
+ resume_task
369
+ ;;
370
+ done)
371
+ echo "Task $TASK_FOLDER is marked done. Use mode 'reopen' to resume it or 'fresh' to start over."
372
+ exit 2
373
+ ;;
374
+ archived)
375
+ echo "Task $TASK_FOLDER is archived. Use mode 'reopen' or 'fresh' explicitly."
376
+ exit 2
377
+ ;;
378
+ *)
379
+ echo "Task $TASK_FOLDER has unknown status '$STATUS'; resuming conservatively."
380
+ resume_task
381
+ ;;
382
+ esac
383
+ ;;
384
+ reopen)
385
+ reopen_task
386
+ ;;
387
+ fresh|reset)
388
+ fresh_task
389
+ ;;
390
+ esac
391
+ else
392
+ case "$MODE" in
393
+ auto|fresh|reset)
394
+ create_task
395
+ ;;
396
+ resume|reopen)
397
+ echo "Task $TASK_FOLDER does not exist; creating it instead."
398
+ create_task
399
+ ;;
400
+ esac
401
+ fi