@house365/comate 0.2.10 → 0.4.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/README.md +11 -4
- package/bin/init-run.mjs +16 -0
- package/bin/init.mjs +43 -1
- package/docs/COMATE_MANUAL.md +48 -2
- package/package.json +1 -1
- package/skills/comate/SKILL.md +9 -2
- package/skills/comate/reference/decision-points.md +25 -5
- package/skills/comate/scripts/comate-lib.sh +94 -0
- package/skills/comate/scripts/comate.sh +204 -0
- package/skills/comate/tests/comate.bats +197 -0
- package/skills/comate-build/SKILL.md +29 -4
- package/skills/comate-grill/SKILL.md +185 -0
- package/skills/comate-intake/SKILL.md +222 -0
- package/skills/comate-intake/scaffold/docs/README.md +36 -0
- package/skills/comate-intake/scaffold/docs/architecture/README.md +31 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/README.md +48 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/integrations/_TEMPLATE/README.md +49 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/integrations/_TEMPLATE/api-snapshot.md +47 -0
- package/skills/comate-intake/scaffold/docs/knowledge-base/requirements/_templates/PRD.md +97 -0
- package/skills/comate-open/SKILL.md +19 -0
- package/skills/comate-verify/SKILL.md +21 -5
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
# sources <name> check — drift check tracked source files
|
|
18
18
|
# sources <name> list — print the source manifest
|
|
19
19
|
# sources find <file> — reverse lookup: changes citing a file
|
|
20
|
+
# gate <prd> review — create/refresh gate.yaml next to the PRD, pin hash
|
|
21
|
+
# gate <prd> set <field> <value> — write score | blocking_open | gate_status | …
|
|
22
|
+
# gate <prd> pass [--tbd] — mark passed (needs blocking_open=0 + report)
|
|
23
|
+
# gate <prd> spawned <change> — record a change fanned out from this PRD
|
|
24
|
+
# gate <prd> status [--json] — print gate status (computes stale on hash drift)
|
|
25
|
+
# gate <prd> check — precondition guard before spawning a change
|
|
20
26
|
# archive <name> [--dry-run] — sync delta specs + annotate + move + mark archived
|
|
21
27
|
# status [path] — list active changes + next command
|
|
22
28
|
# doctor — prerequisite self-check
|
|
@@ -66,6 +72,12 @@ Usage: comate.sh <command> ...
|
|
|
66
72
|
sources <name> check
|
|
67
73
|
sources <name> list
|
|
68
74
|
sources find <file>
|
|
75
|
+
gate <prd> review create/refresh gate.yaml next to PRD, pin hash
|
|
76
|
+
gate <prd> set <field> <value> score | blocking_open | gate_status | …
|
|
77
|
+
gate <prd> pass [--tbd] mark passed (needs blocking_open=0 + report)
|
|
78
|
+
gate <prd> spawned <change> record a change fanned out from this PRD
|
|
79
|
+
gate <prd> status [--json]
|
|
80
|
+
gate <prd> check precondition guard before spawning a change
|
|
69
81
|
archive <name> [--dry-run]
|
|
70
82
|
status [path]
|
|
71
83
|
doctor
|
|
@@ -1332,6 +1344,184 @@ sources_find() {
|
|
|
1332
1344
|
return 0
|
|
1333
1345
|
}
|
|
1334
1346
|
|
|
1347
|
+
# =====================================================================
|
|
1348
|
+
# gate — phase 0: PRD quality gate + split precondition
|
|
1349
|
+
# =====================================================================
|
|
1350
|
+
|
|
1351
|
+
# require_prd <prd> — ensure the PRD path is a real, non-empty file; echo it.
|
|
1352
|
+
require_prd() {
|
|
1353
|
+
local prd="$1"
|
|
1354
|
+
[ -n "$prd" ] || die "$COMATE_USAGE" "ERROR: PRD path required"
|
|
1355
|
+
case "$prd" in *..*) die "$COMATE_USAGE" "ERROR: PRD path cannot contain '..': $prd" ;; esac
|
|
1356
|
+
[ -f "$prd" ] || die "$COMATE_CORRUPT" "ERROR: PRD not found: $prd"
|
|
1357
|
+
file_nonempty "$prd" || die "$COMATE_CORRUPT" "ERROR: PRD is empty: $prd"
|
|
1358
|
+
echo "$prd"
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
# gate <prd> review — create or refresh gate.yaml next to the PRD and pin the
|
|
1362
|
+
# current PRD hash. Existing passed gates are re-opened to `reviewing` only when
|
|
1363
|
+
# the PRD changed (hash drift); otherwise the review report path is (re)emitted
|
|
1364
|
+
# so the skill knows where to write its human-readable audit. The skill does the
|
|
1365
|
+
# semantic scoring and writes score/blocking_open/gate_status via `gate set`.
|
|
1366
|
+
gate_review() {
|
|
1367
|
+
local prd; prd=$(require_prd "$1")
|
|
1368
|
+
local gate report cur_hash
|
|
1369
|
+
gate=$(gate_path_for "$prd")
|
|
1370
|
+
report=$(gate_report_for "$prd")
|
|
1371
|
+
cur_hash=$(hash_file "$prd")
|
|
1372
|
+
|
|
1373
|
+
if [ ! -f "$gate" ]; then
|
|
1374
|
+
cat > "$gate" <<EOF
|
|
1375
|
+
# comate PRD gate — flat key:value only, no nesting
|
|
1376
|
+
schema_version: $GATE_SCHEMA_VERSION
|
|
1377
|
+
prd_path: $prd
|
|
1378
|
+
prd_hash: $cur_hash
|
|
1379
|
+
gate_status: reviewing
|
|
1380
|
+
score: null
|
|
1381
|
+
blocking_open: null
|
|
1382
|
+
review_report: $report
|
|
1383
|
+
spawned_changes: null
|
|
1384
|
+
gated_at: null
|
|
1385
|
+
created_at: $(today)
|
|
1386
|
+
EOF
|
|
1387
|
+
log_pass "[GATE] created $gate (status=reviewing)"
|
|
1388
|
+
else
|
|
1389
|
+
local old_hash old_status
|
|
1390
|
+
old_hash=$(yaml_get "$gate" prd_hash)
|
|
1391
|
+
old_status=$(yaml_get "$gate" gate_status)
|
|
1392
|
+
gate_yaml_set "$gate" prd_path "$prd"
|
|
1393
|
+
gate_yaml_set "$gate" review_report "$report"
|
|
1394
|
+
if [ "$old_hash" != "$cur_hash" ]; then
|
|
1395
|
+
gate_yaml_set "$gate" prd_hash "$cur_hash"
|
|
1396
|
+
gate_yaml_set "$gate" gate_status reviewing
|
|
1397
|
+
log_warn "[GATE] PRD changed since last review — status reset to reviewing (was $old_status)"
|
|
1398
|
+
else
|
|
1399
|
+
log_info "[GATE] $gate (status=$old_status, unchanged PRD)"
|
|
1400
|
+
fi
|
|
1401
|
+
fi
|
|
1402
|
+
echo "review_report: $report"
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
# gate <prd> set <field> <value> — write a gate field (score/blocking_open/
|
|
1406
|
+
# gate_status/…) with the same validation as gate_yaml_set.
|
|
1407
|
+
gate_set() {
|
|
1408
|
+
local prd; prd=$(require_prd "$1")
|
|
1409
|
+
local field="${2:-}" value="${3:-}"
|
|
1410
|
+
[ -n "$field" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> set <field> <value>"
|
|
1411
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1412
|
+
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
|
|
1413
|
+
gate_yaml_set "$gate" "$field" "$value"
|
|
1414
|
+
log_pass "[GATE] set $field=$value"
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
# gate <prd> pass [--tbd] — flip to passed/passed-with-tbd. Hard precondition:
|
|
1418
|
+
# blocking_open must be 0 (no open blocking issues). Re-pins prd_hash so a later
|
|
1419
|
+
# edit invalidates the pass. --tbd records that the user authorised proceeding
|
|
1420
|
+
# with unresolved TBDs (spawned changes must carry ⚠ markers).
|
|
1421
|
+
gate_pass() {
|
|
1422
|
+
local prd; prd=$(require_prd "$1")
|
|
1423
|
+
local tbd=0
|
|
1424
|
+
[ "${2:-}" = "--tbd" ] && tbd=1
|
|
1425
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1426
|
+
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found (run 'gate review' first): $gate"
|
|
1427
|
+
|
|
1428
|
+
local blocking; blocking=$(yaml_get "$gate" blocking_open)
|
|
1429
|
+
if [ "$blocking" != "0" ]; then
|
|
1430
|
+
die "$COMATE_BLOCK" "BLOCK: blocking_open='$blocking' — resolve all blocking issues (set blocking_open 0) before passing the gate"
|
|
1431
|
+
fi
|
|
1432
|
+
local report; report=$(yaml_get "$gate" review_report)
|
|
1433
|
+
if [ -z "$report" ] || [ "$report" = "null" ] || ! file_nonempty "$report"; then
|
|
1434
|
+
die "$COMATE_BLOCK" "BLOCK: review report missing or empty ($report) — the gate report is mandatory"
|
|
1435
|
+
fi
|
|
1436
|
+
|
|
1437
|
+
gate_yaml_set "$gate" prd_hash "$(hash_file "$prd")"
|
|
1438
|
+
if [ "$tbd" -eq 1 ]; then
|
|
1439
|
+
gate_yaml_set "$gate" gate_status passed-with-tbd
|
|
1440
|
+
else
|
|
1441
|
+
gate_yaml_set "$gate" gate_status passed
|
|
1442
|
+
fi
|
|
1443
|
+
gate_yaml_set "$gate" gated_at "$(today)"
|
|
1444
|
+
log_pass "[GATE] $(yaml_get "$gate" gate_status) — changes may now be spawned from $prd"
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
# gate <prd> spawned <change> — append a change name to spawned_changes
|
|
1448
|
+
# (comma-joined), recording which changes this PRD fanned out into.
|
|
1449
|
+
gate_spawned() {
|
|
1450
|
+
local prd; prd=$(require_prd "$1")
|
|
1451
|
+
local change="${2:-}"
|
|
1452
|
+
validate_change_name "$change"
|
|
1453
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1454
|
+
[ -f "$gate" ] || die "$COMATE_CORRUPT" "ERROR: gate not found: $gate"
|
|
1455
|
+
local cur; cur=$(yaml_get "$gate" spawned_changes)
|
|
1456
|
+
case ",$cur," in
|
|
1457
|
+
*",$change,"*) log_info "[GATE] $change already recorded" ; return 0 ;;
|
|
1458
|
+
esac
|
|
1459
|
+
if [ -z "$cur" ] || [ "$cur" = "null" ]; then
|
|
1460
|
+
gate_yaml_set "$gate" spawned_changes "$change"
|
|
1461
|
+
else
|
|
1462
|
+
gate_yaml_set "$gate" spawned_changes "$cur,$change"
|
|
1463
|
+
fi
|
|
1464
|
+
log_pass "[GATE] recorded spawned change: $change"
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
# _gate_is_stale <gate> <prd> — true (0) if the recorded hash no longer matches
|
|
1468
|
+
# the PRD on disk.
|
|
1469
|
+
_gate_is_stale() {
|
|
1470
|
+
local gate="$1" prd="$2" recorded actual
|
|
1471
|
+
recorded=$(yaml_get "$gate" prd_hash)
|
|
1472
|
+
[ -f "$prd" ] || return 0
|
|
1473
|
+
actual=$(hash_file "$prd")
|
|
1474
|
+
[ "$recorded" != "$actual" ]
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
# gate <prd> status [--json] — print current gate status, computing stale on the
|
|
1478
|
+
# fly (hash drift). Does not mutate the file.
|
|
1479
|
+
gate_status() {
|
|
1480
|
+
local prd="$1" json="${2:-}"
|
|
1481
|
+
[ -n "$prd" ] || die "$COMATE_USAGE" "Usage: comate.sh gate <prd> status [--json]"
|
|
1482
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1483
|
+
if [ ! -f "$gate" ]; then
|
|
1484
|
+
if [ "$json" = "--json" ]; then
|
|
1485
|
+
printf '{"gate":"none","prd":"%s"}\n' "$(json_escape "$prd")"
|
|
1486
|
+
else
|
|
1487
|
+
echo "gate: none ($prd) — run 'gate review' to start"
|
|
1488
|
+
fi
|
|
1489
|
+
return "$COMATE_OK"
|
|
1490
|
+
fi
|
|
1491
|
+
local st score blocking; st=$(yaml_get "$gate" gate_status)
|
|
1492
|
+
score=$(yaml_get "$gate" score); blocking=$(yaml_get "$gate" blocking_open)
|
|
1493
|
+
if [ -f "$prd" ] && _gate_is_stale "$gate" "$prd"; then st="stale"; fi
|
|
1494
|
+
if [ "$json" = "--json" ]; then
|
|
1495
|
+
printf '{"gate":"%s","status":"%s","score":"%s","blocking_open":"%s","report":"%s"}\n' \
|
|
1496
|
+
"$(json_escape "$gate")" "$(json_escape "$st")" "$(json_escape "$score")" \
|
|
1497
|
+
"$(json_escape "$blocking")" "$(json_escape "$(yaml_get "$gate" review_report)")"
|
|
1498
|
+
else
|
|
1499
|
+
echo "gate: $st score=$score blocking_open=$blocking ($gate)"
|
|
1500
|
+
fi
|
|
1501
|
+
return "$COMATE_OK"
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
# gate <prd> check — the precondition guard comate-open runs before spawning a
|
|
1505
|
+
# change. PASS (exit 0) requires: gate.yaml exists, status ∈ {passed,
|
|
1506
|
+
# passed-with-tbd}, and prd_hash matches the current PRD. Anything else BLOCKs.
|
|
1507
|
+
gate_check() {
|
|
1508
|
+
local prd; prd=$(require_prd "$1")
|
|
1509
|
+
local gate; gate=$(gate_path_for "$prd")
|
|
1510
|
+
if [ ! -f "$gate" ]; then
|
|
1511
|
+
die "$COMATE_BLOCK" "BLOCK: no gate for $prd — run /comate-intake to review the PRD before creating a change"
|
|
1512
|
+
fi
|
|
1513
|
+
local st; st=$(yaml_get "$gate" gate_status)
|
|
1514
|
+
case "$st" in
|
|
1515
|
+
passed|passed-with-tbd) ;;
|
|
1516
|
+
*) die "$COMATE_BLOCK" "BLOCK: gate_status='$st' (not passed) — a change cannot be spawned until the PRD gate passes" ;;
|
|
1517
|
+
esac
|
|
1518
|
+
if _gate_is_stale "$gate" "$prd"; then
|
|
1519
|
+
die "$COMATE_BLOCK" "BLOCK: PRD changed since it passed the gate (hash mismatch) — re-run /comate-intake to re-gate before spawning a change"
|
|
1520
|
+
fi
|
|
1521
|
+
log_pass "[GATE] check PASS ($st) — $prd"
|
|
1522
|
+
return "$COMATE_OK"
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1335
1525
|
# =====================================================================
|
|
1336
1526
|
# archive
|
|
1337
1527
|
# =====================================================================
|
|
@@ -1678,6 +1868,20 @@ case "$CMD" in
|
|
|
1678
1868
|
esac
|
|
1679
1869
|
fi
|
|
1680
1870
|
;;
|
|
1871
|
+
gate)
|
|
1872
|
+
PRD="${1:-}"; SUB="${2:-}"
|
|
1873
|
+
[ -n "$PRD" ] && [ -n "$SUB" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1874
|
+
shift 2 || true
|
|
1875
|
+
case "$SUB" in
|
|
1876
|
+
review) gate_review "$PRD" ;;
|
|
1877
|
+
set) gate_set "$PRD" "${1:-}" "${2:-}" ;;
|
|
1878
|
+
pass) gate_pass "$PRD" "${1:-}" ;;
|
|
1879
|
+
spawned) gate_spawned "$PRD" "${1:-}" ;;
|
|
1880
|
+
status) gate_status "$PRD" "${1:-}" ;;
|
|
1881
|
+
check) gate_check "$PRD" ;;
|
|
1882
|
+
*) die "$COMATE_USAGE" "Unknown gate subcommand: $SUB" ;;
|
|
1883
|
+
esac
|
|
1884
|
+
;;
|
|
1681
1885
|
archive)
|
|
1682
1886
|
[ -n "${1:-}" ] || { usage_top; exit "$COMATE_USAGE"; }
|
|
1683
1887
|
archive "$1" "${2:-}"
|
|
@@ -447,3 +447,200 @@ SH
|
|
|
447
447
|
run comate frobnicate
|
|
448
448
|
[ "$status" -eq 2 ]
|
|
449
449
|
}
|
|
450
|
+
|
|
451
|
+
# --- gate ---
|
|
452
|
+
|
|
453
|
+
seed_prd() {
|
|
454
|
+
mkdir -p docs/knowledge-base/requirements/test
|
|
455
|
+
printf '# PRD-001\n功能清单:登录、登出\n' > \
|
|
456
|
+
docs/knowledge-base/requirements/test/PRD-001-demo.md
|
|
457
|
+
echo "docs/knowledge-base/requirements/test/PRD-001-demo.md"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@test "gate review creates gate.yaml next to PRD" {
|
|
461
|
+
local prd; prd=$(seed_prd)
|
|
462
|
+
run comate gate "$prd" review
|
|
463
|
+
[ "$status" -eq 0 ]
|
|
464
|
+
[ -f "docs/knowledge-base/requirements/test/PRD-001-demo.gate.yaml" ]
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@test "gate review writes reviewing status and pins prd_hash" {
|
|
468
|
+
local prd; prd=$(seed_prd)
|
|
469
|
+
comate gate "$prd" review >/dev/null
|
|
470
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001-demo.gate.yaml"
|
|
471
|
+
run comate gate "$prd" status
|
|
472
|
+
[ "$status" -eq 0 ]
|
|
473
|
+
[[ "$output" == *"reviewing"* ]]
|
|
474
|
+
local hash; hash=$(grep '^prd_hash:' "$gate" | awk '{print $2}')
|
|
475
|
+
[ -n "$hash" ]
|
|
476
|
+
[ "$hash" != "null" ]
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@test "gate check blocks when status is reviewing" {
|
|
480
|
+
local prd; prd=$(seed_prd)
|
|
481
|
+
comate gate "$prd" review >/dev/null
|
|
482
|
+
run comate gate "$prd" check
|
|
483
|
+
[ "$status" -eq 1 ]
|
|
484
|
+
[[ "$output" == *"BLOCK"* ]]
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@test "gate pass blocked when blocking_open not zero" {
|
|
488
|
+
local prd; prd=$(seed_prd)
|
|
489
|
+
comate gate "$prd" review >/dev/null
|
|
490
|
+
comate gate "$prd" set score 80 >/dev/null
|
|
491
|
+
run comate gate "$prd" pass
|
|
492
|
+
[ "$status" -eq 1 ]
|
|
493
|
+
[[ "$output" == *"blocking_open"* ]]
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@test "gate pass blocked when review report missing" {
|
|
497
|
+
local prd; prd=$(seed_prd)
|
|
498
|
+
comate gate "$prd" review >/dev/null
|
|
499
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
500
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
501
|
+
run comate gate "$prd" pass
|
|
502
|
+
[ "$status" -eq 1 ]
|
|
503
|
+
[[ "$output" == *"report"* ]]
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
@test "gate pass succeeds and check passes after report written" {
|
|
507
|
+
local prd; prd=$(seed_prd)
|
|
508
|
+
comate gate "$prd" review >/dev/null
|
|
509
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
510
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
511
|
+
printf '# 评审报告\n' > \
|
|
512
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
513
|
+
run comate gate "$prd" pass
|
|
514
|
+
[ "$status" -eq 0 ]
|
|
515
|
+
run comate gate "$prd" check
|
|
516
|
+
[ "$status" -eq 0 ]
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@test "gate pass --tbd sets passed-with-tbd" {
|
|
520
|
+
local prd; prd=$(seed_prd)
|
|
521
|
+
comate gate "$prd" review >/dev/null
|
|
522
|
+
comate gate "$prd" set score 65 >/dev/null
|
|
523
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
524
|
+
printf '# 评审报告\n' > \
|
|
525
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
526
|
+
run comate gate "$prd" pass --tbd
|
|
527
|
+
[ "$status" -eq 0 ]
|
|
528
|
+
run comate gate "$prd" status
|
|
529
|
+
[[ "$output" == *"passed-with-tbd"* ]]
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@test "gate check blocks after PRD modified post-pass (stale)" {
|
|
533
|
+
local prd; prd=$(seed_prd)
|
|
534
|
+
comate gate "$prd" review >/dev/null
|
|
535
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
536
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
537
|
+
printf '# 评审报告\n' > \
|
|
538
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
539
|
+
comate gate "$prd" pass >/dev/null
|
|
540
|
+
# mutate PRD after passing
|
|
541
|
+
printf '\n额外内容\n' >> "$prd"
|
|
542
|
+
run comate gate "$prd" check
|
|
543
|
+
[ "$status" -eq 1 ]
|
|
544
|
+
[[ "$output" == *"stale"* ]] || [[ "$output" == *"hash mismatch"* ]]
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@test "gate status reports stale on hash drift without mutating file" {
|
|
548
|
+
local prd; prd=$(seed_prd)
|
|
549
|
+
comate gate "$prd" review >/dev/null
|
|
550
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
551
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
552
|
+
printf '# 评审报告\n' > \
|
|
553
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
554
|
+
comate gate "$prd" pass >/dev/null
|
|
555
|
+
printf '\n改动\n' >> "$prd"
|
|
556
|
+
run comate gate "$prd" status
|
|
557
|
+
[ "$status" -eq 0 ]
|
|
558
|
+
[[ "$output" == *"stale"* ]]
|
|
559
|
+
# gate.yaml should still say passed (status is computed, not written by status cmd)
|
|
560
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001-demo.gate.yaml"
|
|
561
|
+
local stored; stored=$(grep '^gate_status:' "$gate" | awk '{print $2}')
|
|
562
|
+
[ "$stored" = "passed" ]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@test "gate spawned records change name in spawned_changes" {
|
|
566
|
+
local prd; prd=$(seed_prd)
|
|
567
|
+
comate gate "$prd" review >/dev/null
|
|
568
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
569
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
570
|
+
printf '# 报告\n' > \
|
|
571
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
572
|
+
comate gate "$prd" pass >/dev/null
|
|
573
|
+
run comate gate "$prd" spawned login-flow
|
|
574
|
+
[ "$status" -eq 0 ]
|
|
575
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001-demo.gate.yaml"
|
|
576
|
+
local sc; sc=$(grep '^spawned_changes:' "$gate" | sed 's/spawned_changes: //')
|
|
577
|
+
[[ "$sc" == *"login-flow"* ]]
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@test "gate spawned appends second change comma-joined" {
|
|
581
|
+
local prd; prd=$(seed_prd)
|
|
582
|
+
comate gate "$prd" review >/dev/null
|
|
583
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
584
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
585
|
+
printf '# 报告\n' > \
|
|
586
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
587
|
+
comate gate "$prd" pass >/dev/null
|
|
588
|
+
comate gate "$prd" spawned login-flow >/dev/null
|
|
589
|
+
comate gate "$prd" spawned logout-flow >/dev/null
|
|
590
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001-demo.gate.yaml"
|
|
591
|
+
local sc; sc=$(grep '^spawned_changes:' "$gate" | sed 's/spawned_changes: //')
|
|
592
|
+
[[ "$sc" == *"login-flow"* ]]
|
|
593
|
+
[[ "$sc" == *"logout-flow"* ]]
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@test "gate status returns none when gate does not exist" {
|
|
597
|
+
mkdir -p docs/knowledge-base/requirements/test
|
|
598
|
+
run comate gate "docs/knowledge-base/requirements/test/PRD-001-demo.md" status
|
|
599
|
+
[ "$status" -eq 0 ]
|
|
600
|
+
[[ "$output" == *"none"* ]]
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
@test "gate unknown subcommand returns usage error" {
|
|
604
|
+
local prd; prd=$(seed_prd)
|
|
605
|
+
run comate gate "$prd" frobnicate
|
|
606
|
+
[ "$status" -eq 2 ]
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
@test "gate review re-opens reviewing when PRD changes after pass" {
|
|
610
|
+
local prd; prd=$(seed_prd)
|
|
611
|
+
comate gate "$prd" review >/dev/null
|
|
612
|
+
comate gate "$prd" set score 90 >/dev/null
|
|
613
|
+
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
614
|
+
printf '# 报告\n' > \
|
|
615
|
+
"docs/knowledge-base/requirements/test/PRD-REVIEW-PRD-001-demo.md"
|
|
616
|
+
comate gate "$prd" pass >/dev/null
|
|
617
|
+
printf '\n改动\n' >> "$prd"
|
|
618
|
+
# re-run review after PRD change — should reset to reviewing
|
|
619
|
+
run comate gate "$prd" review
|
|
620
|
+
[ "$status" -eq 0 ]
|
|
621
|
+
run comate gate "$prd" status
|
|
622
|
+
[[ "$output" == *"reviewing"* ]]
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@test "gate set rejects unknown field" {
|
|
626
|
+
local prd; prd=$(seed_prd)
|
|
627
|
+
comate gate "$prd" review >/dev/null
|
|
628
|
+
run comate gate "$prd" set nonexistent_field foo
|
|
629
|
+
[ "$status" -eq 2 ]
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
@test "gate set rejects invalid gate_status value" {
|
|
633
|
+
local prd; prd=$(seed_prd)
|
|
634
|
+
comate gate "$prd" review >/dev/null
|
|
635
|
+
run comate gate "$prd" set gate_status flying
|
|
636
|
+
[ "$status" -eq 2 ]
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@test "gate status --json emits json with status field" {
|
|
640
|
+
local prd; prd=$(seed_prd)
|
|
641
|
+
comate gate "$prd" review >/dev/null
|
|
642
|
+
run comate gate "$prd" status --json
|
|
643
|
+
[ "$status" -eq 0 ]
|
|
644
|
+
[[ "$output" == *'"status"'* ]]
|
|
645
|
+
[[ "$output" == *'"reviewing"'* ]]
|
|
646
|
+
}
|
|
@@ -54,13 +54,38 @@ bash "$COMATE" state <name> set plan docs/superpowers/plans/YYYY-MM-DD-feature.m
|
|
|
54
54
|
|
|
55
55
|
### 3. 选择工作方式(决策阻塞点 3)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
就工作区隔离、执行方式、TDD 模式做**一个联合决策**。规则与判定协议见 `comate/reference/decision-points.md`(先判定"三类行动",第 3 条)。
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
#### 3a. 能力预检(展示选项前先做)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
逐维度检查当前平台/仓库实际可执行的选项,只保留可执行者:
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
- **isolation**(脚本枚举仅 `branch` / `worktree`)
|
|
64
|
+
- `worktree`:仅当 `superpowers:using-git-worktrees` skill 可用时保留;不可用则剔除。
|
|
65
|
+
- `branch`:始终可用(HEAD detached 时先提示用户切到实分支)。
|
|
66
|
+
- **build_mode**
|
|
67
|
+
- `subagent-driven-development`:仅当当前平台有真实后台子 agent / Task 派发能力时保留;无则剔除。
|
|
68
|
+
- `executing-plans`:始终可用。
|
|
69
|
+
- **tdd_mode**:`tdd` / `direct` 始终可用。
|
|
70
|
+
|
|
71
|
+
#### 3b. 唯一选项自动应用(属"自动处理",不阻塞)
|
|
72
|
+
|
|
73
|
+
预检后某维度只剩一个可执行选项,**直接写入并说明原因,不为它单独问用户**。例如:
|
|
74
|
+
|
|
75
|
+
- 仅 `executing-plans` 可用(无后台派发能力)→ 自动写 `build_mode: executing-plans`,说明"无后台子 agent 能力"。
|
|
76
|
+
- 仅 `branch` 可用(`using-git-worktrees` 未安装)→ 自动写 `isolation: branch`,说明"worktree skill 不可用"。
|
|
77
|
+
|
|
78
|
+
多个维度都只剩单值时,一并自动写入并逐条说明;三个维度全部只剩单值时本决策点整体跳过,直接进入 Step 3c 执行。
|
|
79
|
+
|
|
80
|
+
#### 3c. 联合决策(仍有 ≥2 有效选项的维度才问)
|
|
81
|
+
|
|
82
|
+
对仍有多个有效选项的维度,**一次 AskUserQuestion 问清全部**,选 `branch` 时**分支名也在同一次确认**,不得先问是否继续再二次追问配置或命名。已在 3b 自动写入的维度不再列入问题。每个选项附推荐理由(推荐只说明倾向,不代替用户拍板):
|
|
83
|
+
|
|
84
|
+
- **工作区隔离**:`branch` 新建分支(简单快速,≤3 文件推荐)/ `worktree` 独立工作区(需并行或当前分支有未提交工作时推荐)。
|
|
85
|
+
- **执行方式**:`subagent-driven-development`(任务 ≥3、独立、需双阶段审查)/ `executing-plans`(任务 ≤2 且无跨模块依赖,或来自 hotfix)。
|
|
86
|
+
- **TDD 模式**:`tdd`(涉及业务逻辑/新功能/API,推荐)/ `direct`(无需测试覆盖或用户明确跳过)。
|
|
87
|
+
|
|
88
|
+
用户选择后(连同 3b 已自动应用的维度)写入:
|
|
64
89
|
|
|
65
90
|
```bash
|
|
66
91
|
bash "$COMATE" state <name> set isolation <branch|worktree>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: comate-grill
|
|
3
|
+
description: "决策点穷追式澄清工具。对计划/设计/PRD 问题进行深度优先遍历,逐个决策点给推荐答案并等用户确认,直到所有歧义清零。可由 comate-intake 内部调用,或用户直接 /comate-grill 使用。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# comate-grill —— 决策点穷追式澄清
|
|
7
|
+
|
|
8
|
+
**定位**:一个通用的决策澄清引擎,用于在计划/设计/需求阶段对模糊点、依赖链、歧义进行**逐个深挖**,直到所有分支清晰。
|
|
9
|
+
|
|
10
|
+
**算法来源**:改编自社区 [grill-me pattern](https://github.com/mattpocock/skills/blob/main/skills/productivity/grill-me/)(Timothy Vang、Matt Pocock 等贡献的最佳实践),内化到 comate 家族以避免外部依赖漂移。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 核心算法(四条铁律)
|
|
15
|
+
|
|
16
|
+
1. **决策树深度优先遍历**
|
|
17
|
+
串行解决决策点,不批量轰炸。后续分支常依赖前面的答案,所以必须一个接一个。
|
|
18
|
+
|
|
19
|
+
2. **每个问题附推荐答案**
|
|
20
|
+
不是裸问题("你觉得 X 怎么设计?"),而是"我建议用 Y 方案,因为 Z 理由。你同意吗?如果不同意,你倾向什么?"
|
|
21
|
+
|
|
22
|
+
3. **优先探索代码库/文档,不问能查到的事实**
|
|
23
|
+
- 已有架构约定、字段定义、接口规范、对接档案 → 用 Read/Grep/Glob/LSP 核实,引用来源,不丢给用户。
|
|
24
|
+
- 产品决策(范围、优先级、用户行为、状态流转、数据口径、失败处理、验收规则)→ 必须问用户。
|
|
25
|
+
|
|
26
|
+
4. **一次一问,等回答再进下一个**
|
|
27
|
+
并行提多个问题会让用户不知从哪答起,且前面的答案可能让后面的问题失效。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 调用场景
|
|
32
|
+
|
|
33
|
+
### 由其他 skill 内部调用(主场景)
|
|
34
|
+
|
|
35
|
+
- **comate-intake 步骤 3(阻断闸门)**:当 PRD 阻断问题 > 3 个或有决策依赖链时,用户可选「深度澄清模式」,此时 intake 调用本 skill,传入阻断问题清单,逐个深挖。
|
|
36
|
+
- **未来扩展**:comate-design 的方案澄清、comate-verify 的验收标准澄清等,都可复用本 skill。
|
|
37
|
+
|
|
38
|
+
**调用方式**(从其他 skill 内):
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+
用 Skill 工具加载 `comate-grill`,传入:
|
|
42
|
+
|
|
43
|
+
话题:<要澄清的主题,如「PRD 阻断问题清单」>
|
|
44
|
+
问题列表:
|
|
45
|
+
1. [ID] 问题描述 | 原文依据 | 影响范围 | 当前歧义
|
|
46
|
+
2. ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 用户直接调用
|
|
50
|
+
|
|
51
|
+
用户可以在任何阶段直接 `/comate-grill <话题>` 对计划、设计、决策进行穷追式澄清。适用于:
|
|
52
|
+
|
|
53
|
+
- 自己的计划草稿想被挑战一遍
|
|
54
|
+
- 技术方案有多个路径,想逐条过一遍利弊
|
|
55
|
+
- PRD/需求文档的边界不清楚,想被问到无死角
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 工作流
|
|
60
|
+
|
|
61
|
+
### 输入
|
|
62
|
+
|
|
63
|
+
- **话题**:澄清的主题(如「PRD 阻断问题」「技术选型」「API 设计」)
|
|
64
|
+
- **问题列表**(可选,无则从话题自由展开):带 ID、描述、依据、影响的问题清单
|
|
65
|
+
|
|
66
|
+
### 过程
|
|
67
|
+
|
|
68
|
+
1. **建决策树骨架**
|
|
69
|
+
根据问题列表(或从话题展开)识别所有决策点,按依赖关系排序(前面的决策会影响后面的选项)。
|
|
70
|
+
|
|
71
|
+
2. **逐点深挖**
|
|
72
|
+
从第一个决策点开始:
|
|
73
|
+
- 先查代码库/文档/架构约定能否直接回答(能则引用来源,告知用户,进入下一个)
|
|
74
|
+
- 不能则构造推荐答案:2–3 个互斥选项 + 推荐项 + 理由 + 各选项的后果
|
|
75
|
+
- 用 **AskUserQuestion** 提问,等用户回答
|
|
76
|
+
- 记录答案,更新后续决策点的前置条件
|
|
77
|
+
|
|
78
|
+
3. **递归展开新分支**
|
|
79
|
+
用户的回答可能引出新的子问题(如选了"分布式事务"就要问"补偿机制怎么设计")。把新问题插入决策树对应位置,继续深挖。
|
|
80
|
+
|
|
81
|
+
4. **终止条件**
|
|
82
|
+
所有决策点都有明确答案(非"待定""看情况"),或用户主动喊停。
|
|
83
|
+
|
|
84
|
+
### 输出
|
|
85
|
+
|
|
86
|
+
- **决策台账**(markdown 表格):每个问题的 ID、问题、推荐、用户答案、依据/来源
|
|
87
|
+
- **待办事项**(若有):哪些答案需要后续在代码/文档中落地
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Guardrails
|
|
92
|
+
|
|
93
|
+
- **AskUserQuestion 是真阻塞点**:推荐答案、默认值、"通常做法"、历史偏好都不能替代用户明确回答。只有用户选了选项或输入自定义答案,才能进入下一个决策点。
|
|
94
|
+
- **不替用户拍板**:可以给建议、列利弊、引用案例,但最终选择必须等用户确认。
|
|
95
|
+
- **不问能查到的事实**:已有的架构文档、代码约定、接口规范先用工具核实,只问"怎么做"的决策,不问"是什么"的事实。
|
|
96
|
+
- **分支依赖显式标注**:后续问题若依赖前面的答案,在提问时点明"因为你选了 X,所以这里...",让用户理解为什么要问这个。
|
|
97
|
+
- **深度有限,避免无底洞**:单个决策点最多展开 2 层子问题。若发现需要第 3 层,说明原问题拆得不够,停下来重新拆解问题本身。
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 示例
|
|
102
|
+
|
|
103
|
+
**输入**(由 comate-intake 调用):
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
话题:PRD 门禁阻断问题清单
|
|
107
|
+
问题列表:
|
|
108
|
+
1. [B-03] 用户登录失败后的重试机制未明确 | 原文"支持登录" | 影响:异常路径、用户体验
|
|
109
|
+
2. [B-07] 数据同步到第三方失败时的降级策略缺失 | 原文"对接外部 API" | 影响:可用性、数据一致性
|
|
110
|
+
3. [B-11] 管理员权限范围模糊(能否删除其他管理员的数据?)| 原文"管理员可管理所有数据" | 影响:权限设计、安全
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**执行**:
|
|
114
|
+
|
|
115
|
+
1. **问题 1(登录重试)**
|
|
116
|
+
- 先查现有登录模块代码:发现有 `max_retries` 配置但未用
|
|
117
|
+
- 给推荐:"我建议限制 5 次/15 分钟,超过后锁定账户 30 分钟并发邮件通知,理由是防暴力破解。你同意吗?"
|
|
118
|
+
- 用户答:"改成 3 次/10 分钟,锁定 1 小时"
|
|
119
|
+
- 记录,进入下一个
|
|
120
|
+
|
|
121
|
+
2. **问题 2(第三方失败降级)**
|
|
122
|
+
- 查架构文档:无降级策略先例
|
|
123
|
+
- 给推荐:"我建议失败后写本地队列,定时重试 3 次,3 次后人工介入。你倾向什么?"
|
|
124
|
+
- 用户答:"可以,但重试间隔用指数退避"
|
|
125
|
+
- 追问:"指数退避的初始间隔和最大间隔?"(新分支)
|
|
126
|
+
- 用户答:"初始 30s,最大 10min"
|
|
127
|
+
- 记录,进入下一个
|
|
128
|
+
|
|
129
|
+
3. **问题 3(管理员权限)**
|
|
130
|
+
- 查权限表和现有角色定义:只有 `admin` 和 `user` 两角色
|
|
131
|
+
- 给推荐:"建议引入超级管理员(super_admin)和普通管理员(admin),普通管理员不能删除其他管理员的数据。你同意吗?"
|
|
132
|
+
- 用户答:"不需要超级管理员,所有管理员平级,但删除操作需二次确认"
|
|
133
|
+
- 记录
|
|
134
|
+
|
|
135
|
+
**输出**:
|
|
136
|
+
|
|
137
|
+
```markdown
|
|
138
|
+
## 决策台账
|
|
139
|
+
|
|
140
|
+
| ID | 问题 | 推荐 | 用户答案 | 依据/来源 |
|
|
141
|
+
|-------|------|------|----------|-----------|
|
|
142
|
+
| B-03 | 登录重试机制 | 5次/15分钟,锁定30分钟 | 3次/10分钟,锁定1小时 | 用户确认 |
|
|
143
|
+
| B-07 | 第三方失败降级 | 本地队列+定时重试3次 | 同意,重试用指数退避(30s→10min) | 用户确认 |
|
|
144
|
+
| B-11 | 管理员权限范围 | 引入super_admin和admin两级 | 不分级,删除操作二次确认 | 用户确认 |
|
|
145
|
+
|
|
146
|
+
所有阻断问题已澄清,可更新 PRD 评审报告并置 `blocking_open=0`。
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 与 comate 其他 skill 的区别
|
|
152
|
+
|
|
153
|
+
| skill | 定位 | 提问方式 |
|
|
154
|
+
|-------|------|----------|
|
|
155
|
+
| **comate-grill** | 决策点穷追引擎,逐个深挖直到无歧义 | 深度优先、一次一问、附推荐 |
|
|
156
|
+
| comate-intake | PRD 门禁+拆分,暴露问题但不深挖每个 | 批量扫描+分轮处理 |
|
|
157
|
+
| openspec-explore | 探索问题空间,发散式头脑风暴 | 开放式、多角度、不强制收敛 |
|
|
158
|
+
| comate-design | 技术设计+方案 brainstorming | 方案对比、架构决策、不逐点穷举 |
|
|
159
|
+
|
|
160
|
+
**简单说**:explore 是发散、grill 是收敛、intake 是门禁、design 是方案对比。grill 的独特价值是"不放过任何一个模糊点"。
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 何时用 comate-grill
|
|
165
|
+
|
|
166
|
+
**推荐用**:
|
|
167
|
+
- 阻断问题 > 3 个且有依赖链(intake 自动提供选项)
|
|
168
|
+
- 技术方案有多条路径,想逐条过利弊(design 阶段手动调用)
|
|
169
|
+
- 自己的计划草稿想被挑战到无死角(用户直接 `/comate-grill <话题>`)
|
|
170
|
+
|
|
171
|
+
**不推荐用**:
|
|
172
|
+
- 简单问题(≤ 2 个独立决策点)→ 直接 AskUserQuestion 即可
|
|
173
|
+
- 纯事实查询("这个字段在哪张表")→ 用 Read/Grep/LSP
|
|
174
|
+
- 开放式头脑风暴("有哪些可能的方案")→ 用 openspec-explore
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 维护说明
|
|
179
|
+
|
|
180
|
+
本 skill 是社区最佳实践的内化版本,避免外部依赖漂移。若社区 grill-me 有新算法改进,可在此同步更新,但保持 comate 命名空间和中文文档风格。
|
|
181
|
+
|
|
182
|
+
**来源致谢**:
|
|
183
|
+
- [mattpocock/skills - grill-me](https://github.com/mattpocock/skills/blob/main/skills/productivity/grill-me/SKILL.md)
|
|
184
|
+
- [gsarig/skills - grill-me](https://github.com/gsarig/skills/blob/main/skills/grill-me/README.md)
|
|
185
|
+
- [RobMitt/grill-me-skill](https://github.com/RobMitt/grill-me-skill)
|