@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.
- package/README.md +154 -0
- package/bin/norpiv-install.cjs +166 -0
- package/cleanup/SKILL.md +53 -0
- package/frame/SKILL.md +38 -0
- package/grill-with-docs/SKILL.md +43 -0
- package/implement/SKILL.md +132 -0
- package/implement/scripts/enforce-branch.sh +122 -0
- package/package.json +49 -0
- package/plan/SKILL.md +34 -0
- package/scripts/reposcry-bootstrap.sh +84 -0
- package/scripts/reposcry-refresh.sh +19 -0
- package/scripts/reposcry-task-context.sh +32 -0
- package/scripts/triage_helper.sh +401 -0
- package/scripts/validate_active_task.sh +328 -0
- package/sync/SKILL.md +131 -0
- package/sync/design-brief.md +39 -0
- package/sync/jira_smart_sync.sh +156 -0
- package/triage/SKILL.md +102 -0
- package/update-docs/SKILL.md +32 -0
- package/update-docs/references/doc-destination-map.md +31 -0
- package/verify/SKILL.md +32 -0
|
@@ -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
|