@walwal-harness/cli 7.1.57 → 7.1.58
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 +15 -0
- package/HR-Resource/ceo/SKILL.md +48 -17
- package/HR-Resource/cqo/SKILL.md +56 -31
- package/HR-Resource/cto/SKILL.md +19 -25
- package/HR-Resource/hiring/SKILL.md +5 -21
- package/HR-Resource/ops/SKILL.md +12 -2
- package/HR-Resource/resource-manager/SKILL.md +2 -2
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +3 -1
- package/assets/templates/AGENTS-ko.md.template +35 -11
- package/assets/templates/AGENTS.md.template +43 -19
- package/assets/templates/config.json +2 -1
- package/commands/goal.md +10 -8
- package/commands/hot-fix.md +10 -8
- package/commands/submission.md +8 -6
- package/package.json +1 -1
- package/scripts/harness-company-complete.sh +104 -50
- package/scripts/harness-lessons-gate.sh +34 -5
- package/scripts/harness-secret-scan.sh +29 -0
- package/scripts/harness-stop.sh +5 -5
- package/scripts/harness-verify-fingerprint.sh +71 -0
- package/scripts/harness-worker-evidence-validate.sh +96 -9
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# harness-company-complete.sh — mark the current v7 company mission complete.
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
# bash scripts/harness-company-complete.sh <project-root> [reason]
|
|
5
|
+
# bash scripts/harness-company-complete.sh <project-root> [reason] [mission-rel]
|
|
6
6
|
#
|
|
7
7
|
# This is the explicit running -> idle/done transition used by dashboard,
|
|
8
8
|
# runner, hooks, and final CEO handoff paths. It intentionally only updates
|
|
9
|
-
# runtime state; it does not archive
|
|
9
|
+
# runtime state and mission lifecycle; it does not archive mission documents.
|
|
10
10
|
|
|
11
11
|
set -uo pipefail
|
|
12
12
|
|
|
@@ -26,44 +26,6 @@ command -v jq >/dev/null 2>&1 || {
|
|
|
26
26
|
exit 1
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
# Lessons-before-plan gate (AGENTS.md Hard Rule 20). The Stop hook cannot see
|
|
30
|
-
# this path: once this script sets conductor.state=completed, harness-stop.sh
|
|
31
|
-
# short-circuits at the top, so a mission could complete having never read the
|
|
32
|
-
# corpus simply by firing the transition. Refuse the terminal transition here —
|
|
33
|
-
# this is the transition P6 names, and it is the last point at which refusing
|
|
34
|
-
# still means anything.
|
|
35
|
-
#
|
|
36
|
-
# Safe against deadlock: the gate is scoped to the latest ACTIVE mission, so the
|
|
37
|
-
# stop-hook backstop (which fires only when no mission is active) always passes.
|
|
38
|
-
# Opt out per project with .harness/config.json behavior.lessons_gate=false, or
|
|
39
|
-
# override a single call with HARNESS_SKIP_LESSONS_GATE=1.
|
|
40
|
-
if [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != "1" ] && [ -x "$SCRIPT_DIR/harness-lessons-gate.sh" ]; then
|
|
41
|
-
if ! gate_out="$(bash "$SCRIPT_DIR/harness-lessons-gate.sh" "$PROJECT_ROOT" text latest-active 2>/dev/null)"; then
|
|
42
|
-
{
|
|
43
|
-
echo "[company-complete] REFUSED: the active mission has not recorded what it read before planning."
|
|
44
|
-
echo "$gate_out"
|
|
45
|
-
echo " Add the two sections to each role document, then re-run this transition."
|
|
46
|
-
echo " (override: HARNESS_SKIP_LESSONS_GATE=1, or behavior.lessons_gate=false in .harness/config.json)"
|
|
47
|
-
} >&2
|
|
48
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | company-complete | refused | lessons-gate (Hard Rule 20)" >> "$PROJECT_ROOT/.harness/progress.log" 2>/dev/null || true
|
|
49
|
-
exit 1
|
|
50
|
-
fi
|
|
51
|
-
fi
|
|
52
|
-
|
|
53
|
-
# Corpus reachability (Hard Rule 11) and spec pins (Hard Rule 4) are re-checked
|
|
54
|
-
# at the same point, for the same reason: both are promises that decay silently
|
|
55
|
-
# between when they are made and when the mission claims to be done.
|
|
56
|
-
if [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != "1" ] && [ -x "$SCRIPT_DIR/harness-corpus-reachability.sh" ]; then
|
|
57
|
-
if ! reach_out="$(bash "$SCRIPT_DIR/harness-corpus-reachability.sh" "$PROJECT_ROOT" text 2>/dev/null)"; then
|
|
58
|
-
{
|
|
59
|
-
echo "[company-complete] REFUSED: corpus entries are unreachable by roles they name as an audience."
|
|
60
|
-
echo "$reach_out"
|
|
61
|
-
} >&2
|
|
62
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | company-complete | refused | corpus-reachability (Hard Rule 11)" >> "$PROJECT_ROOT/.harness/progress.log" 2>/dev/null || true
|
|
63
|
-
exit 1
|
|
64
|
-
fi
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
29
|
state_mtime() {
|
|
68
30
|
stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0
|
|
69
31
|
}
|
|
@@ -87,24 +49,117 @@ pick_transition_mission_state() {
|
|
|
87
49
|
[ -n "$best" ] && printf '%s\n' "$best"
|
|
88
50
|
}
|
|
89
51
|
|
|
52
|
+
refuse() {
|
|
53
|
+
echo "[company-complete] REFUSED: $1" >&2
|
|
54
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | company-complete | refused | $1" >> "$PROJECT_ROOT/.harness/progress.log"
|
|
55
|
+
if [ -n "${target_state:-}" ] && jq -e '.lifecycle == "complete" or .lifecycle == "completed"' "$target_state" >/dev/null; then
|
|
56
|
+
echo 'Restore active:true and lifecycle:active, then retry.' >&2
|
|
57
|
+
fi
|
|
58
|
+
exit 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
target_state=""
|
|
62
|
+
if [ "$#" -ge 3 ]; then
|
|
63
|
+
mission_rel="$3"
|
|
64
|
+
case "$mission_rel" in ""|/*|*..*) refuse invalid-mission-path ;; esac
|
|
65
|
+
target_dir="$(cd "$DOCS/$mission_rel" 2>/dev/null && pwd -P)" || refuse invalid-mission-path
|
|
66
|
+
case "$target_dir/" in "$(cd "$DOCS" && pwd -P)/"*) ;; *) refuse invalid-mission-path ;; esac
|
|
67
|
+
target_state="$target_dir/mission-state.json"
|
|
68
|
+
[ -f "$target_state" ] || refuse missing-mission-state
|
|
69
|
+
else
|
|
70
|
+
target_state="$(pick_transition_mission_state)"
|
|
71
|
+
fi
|
|
72
|
+
ended=false
|
|
73
|
+
tiered=false
|
|
74
|
+
gate_scope=latest-active
|
|
75
|
+
# Explicit targets scope the existing lessons gate even for legacy missions.
|
|
76
|
+
if [ "$#" -ge 3 ]; then gate_scope="mission:$3"; fi
|
|
77
|
+
if [ -n "$target_state" ]; then
|
|
78
|
+
lifecycle=$(jq -r '.lifecycle // .status // "unknown"' "$target_state") || refuse invalid-mission-state
|
|
79
|
+
case "$lifecycle" in cancelled|superseded|closed) ended=true ;; esac
|
|
80
|
+
if jq -e 'has("tier")' "$target_state" >/dev/null; then
|
|
81
|
+
tiered=true
|
|
82
|
+
mission_rel="${target_state#"$DOCS"/}"
|
|
83
|
+
# Explicit targets were canonicalized; retain the caller's relative path.
|
|
84
|
+
if [ "$#" -ge 3 ]; then mission_rel="$3/mission-state.json"; fi
|
|
85
|
+
mission_rel="${mission_rel%/mission-state.json}"
|
|
86
|
+
gate_scope="mission:$mission_rel"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ "$ended" = false ]; then
|
|
91
|
+
if [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != 1 ] && [ -x "$SCRIPT_DIR/harness-lessons-gate.sh" ]; then
|
|
92
|
+
gate_out=$(bash "$SCRIPT_DIR/harness-lessons-gate.sh" "$PROJECT_ROOT" text "$gate_scope") || refuse "lessons-gate: $gate_out"
|
|
93
|
+
fi
|
|
94
|
+
if [ "$tiered" = true ]; then
|
|
95
|
+
evidence=$(bash "$SCRIPT_DIR/harness-worker-evidence-validate.sh" "$PROJECT_ROOT" text "$gate_scope") || refuse "worker-evidence: $evidence"
|
|
96
|
+
mission_dir="$(dirname "$target_state")"
|
|
97
|
+
[ -f "$mission_dir/cqo.md" ] || refuse missing-verdict
|
|
98
|
+
verdict=$(awk '
|
|
99
|
+
/^[[:space:]]*>?[[:space:]]*##[[:space:]]+CQO Verdict([[:space:]]+\([^)]*\))?[[:space:]]*$/ { inb=1; next }
|
|
100
|
+
/^[[:space:]]*>?[[:space:]]*#{1,2}[[:space:]]/ { inb=0 }
|
|
101
|
+
inb { candidate=$0; sub(/^[[:space:]>*_-]*/, "", candidate)
|
|
102
|
+
if (tolower(candidate) ~ /^verdict[*_[:space:]]*:/) last=$0 }
|
|
103
|
+
END {
|
|
104
|
+
if (last == "") print "missing"
|
|
105
|
+
else if (last ~ /^[[:space:]]*>?[[:space:]]*Verdict:[[:space:]]*(PASS|ACCEPTED|FAIL|REJECTED|BLOCKED)[[:space:]]*$/) {
|
|
106
|
+
sub(/^[[:space:]]*>?[[:space:]]*Verdict:[[:space:]]*/, "", last)
|
|
107
|
+
sub(/[[:space:]]*$/, "", last); print last
|
|
108
|
+
} else print "invalid"
|
|
109
|
+
}
|
|
110
|
+
' "$mission_dir/cqo.md") || refuse invalid-final-verdict
|
|
111
|
+
case "$verdict" in
|
|
112
|
+
PASS|ACCEPTED) ;;
|
|
113
|
+
missing) refuse missing-verdict ;;
|
|
114
|
+
invalid) refuse invalid-final-verdict ;;
|
|
115
|
+
*) refuse verdict-not-pass ;;
|
|
116
|
+
esac
|
|
117
|
+
tier=$(jq -r 'def r: if . == "S" then 0 elif . == "M" then 1 else 2 end;
|
|
118
|
+
[.tier, (.tier_history[]? | .from, .to)] | map(r) | max' "$target_state") || refuse invalid-tier-state
|
|
119
|
+
if [ -f "$PROJECT_ROOT/.harness/config.json" ]; then
|
|
120
|
+
enabled=$(jq -r 'if .behavior.mission_tiers == null then true else .behavior.mission_tiers end' "$PROJECT_ROOT/.harness/config.json") || refuse invalid-config
|
|
121
|
+
[ "$enabled" != false ] || tier=2
|
|
122
|
+
fi
|
|
123
|
+
if [ "$tier" -eq 0 ]; then
|
|
124
|
+
grep -Eq '^[[:space:]]*>?[[:space:]]*##[[:space:]]+Direct Work[[:space:]]*$' "$mission_dir/cto.md" || refuse missing-direct-work
|
|
125
|
+
grep -Eq '^[[:space:]]*>?[[:space:]]*##[[:space:]]+Verification Commands[[:space:]]*$' "$mission_dir/cqo.md" || refuse missing-verification-commands
|
|
126
|
+
session=$(sed -nE 's/^[[:space:]]*Verification Session: (separate|same-session)[[:space:]]*$/\1/p' "$mission_dir/cqo.md" | tail -1)
|
|
127
|
+
[ -n "$session" ] || refuse missing-verification-session
|
|
128
|
+
if [ "$session" = same-session ]; then
|
|
129
|
+
echo '[company-complete] WARNING: same-session-verification; disclose to Owner.' >&2
|
|
130
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | company-complete | warn | same-session-verification" >> "$PROJECT_ROOT/.harness/progress.log"
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Corpus reachability (Hard Rule 11) and spec pins (Hard Rule 4) are re-checked
|
|
137
|
+
# at the same point, for the same reason: both are promises that decay silently
|
|
138
|
+
# between when they are made and when the mission claims to be done.
|
|
139
|
+
if [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != "1" ] && [ -x "$SCRIPT_DIR/harness-corpus-reachability.sh" ]; then
|
|
140
|
+
if ! reach_out="$(bash "$SCRIPT_DIR/harness-corpus-reachability.sh" "$PROJECT_ROOT" text 2>/dev/null)"; then
|
|
141
|
+
refuse "corpus-reachability: $reach_out"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
90
145
|
# Spec pins are verified against the mission this transition would close. This
|
|
91
146
|
# sits BEFORE the runtime transition on purpose: a refusal that runs after
|
|
92
147
|
# progress.json is already `completed` has refused nothing.
|
|
93
|
-
if [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != "1" ] && [ -x "$SCRIPT_DIR/harness-spec-pin.sh" ] && [ -d "$DOCS" ]; then
|
|
94
|
-
pin_target="$
|
|
148
|
+
if [ "$ended" = false ] && [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != "1" ] && [ -x "$SCRIPT_DIR/harness-spec-pin.sh" ] && [ -d "$DOCS" ]; then
|
|
149
|
+
pin_target="$target_state"
|
|
95
150
|
if [ -n "$pin_target" ]; then
|
|
96
151
|
mission_rel="${pin_target#"$DOCS"/}"; mission_rel="${mission_rel%/mission-state.json}"
|
|
152
|
+
if [ "$#" -ge 3 ]; then mission_rel="$3"; fi
|
|
97
153
|
if ! pin_out="$(bash "$SCRIPT_DIR/harness-spec-pin.sh" "$PROJECT_ROOT" "$mission_rel" verify text 2>/dev/null)"; then
|
|
98
|
-
|
|
99
|
-
echo "[company-complete] REFUSED: this mission was built against a spec that has since moved."
|
|
100
|
-
echo "$pin_out"
|
|
101
|
-
} >&2
|
|
102
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | company-complete | refused | spec-pin drift (Hard Rule 4)" >> "$PROJECT_ROOT/.harness/progress.log" 2>/dev/null || true
|
|
103
|
-
exit 1
|
|
154
|
+
refuse "spec-pin drift: $pin_out"
|
|
104
155
|
fi
|
|
105
156
|
fi
|
|
106
157
|
fi
|
|
107
158
|
|
|
159
|
+
if [ "$ended" = true ]; then
|
|
160
|
+
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | company-complete | ended-without-acceptance | $lifecycle" >> "$PROJECT_ROOT/.harness/progress.log"
|
|
161
|
+
fi
|
|
162
|
+
|
|
108
163
|
if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
|
|
109
164
|
'.company_state.state = "idle" |
|
|
110
165
|
.company_state.active_workers = 0 |
|
|
@@ -143,11 +198,10 @@ if [ -f "$TODOS" ]; then
|
|
|
143
198
|
fi
|
|
144
199
|
|
|
145
200
|
if [ -d "$DOCS" ]; then
|
|
146
|
-
target_state="$(pick_transition_mission_state)"
|
|
147
201
|
if [ -n "$target_state" ]; then
|
|
148
202
|
lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$target_state" 2>/dev/null || echo unknown)"
|
|
149
203
|
case "$lifecycle" in
|
|
150
|
-
closed|cancelled|superseded
|
|
204
|
+
closed|cancelled|superseded) ;;
|
|
151
205
|
*)
|
|
152
206
|
tmp="$(mktemp)"
|
|
153
207
|
jq '.lifecycle = "complete" | .active = false | .completed_at = (now | todate) | del(.blocked_reason) | del(.blocked_at)' "$target_state" > "$tmp" && mv "$tmp" "$target_state"
|
|
@@ -11,17 +11,19 @@
|
|
|
11
11
|
# ## Lessons Preflight — which convention/gotcha items apply, and why
|
|
12
12
|
# ## Lessons Tally — one line: which of them actually fired ("0 fired" is valid)
|
|
13
13
|
#
|
|
14
|
-
# Usage: harness-lessons-gate.sh <project-root> [text|json] [all|latest-active]
|
|
14
|
+
# Usage: harness-lessons-gate.sh <project-root> [text|json] [all|latest-active|mission:<rel>]
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
PROJECT_ROOT="${1:-.}"
|
|
18
18
|
DOC_ROOT="$PROJECT_ROOT/.harness/documents"
|
|
19
19
|
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
20
20
|
|
|
21
|
-
[ -d "$DOC_ROOT" ] || exit 0
|
|
22
|
-
|
|
23
21
|
mode="${2:-text}"
|
|
24
22
|
scope="${3:-all}"
|
|
23
|
+
if [ ! -d "$DOC_ROOT" ]; then
|
|
24
|
+
[[ "$scope" != mission:* && "$mode" != direct-work-sha ]] || exit 1
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
25
27
|
|
|
26
28
|
# Opt-out for projects that have not adopted the sections yet.
|
|
27
29
|
if command -v jq >/dev/null 2>&1 && [ -f "$CONFIG" ]; then
|
|
@@ -57,7 +59,10 @@ mission_mtime() {
|
|
|
57
59
|
stat -f '%m' "$1" 2>/dev/null || stat -c '%Y' "$1" 2>/dev/null || echo 0
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
mission_dirs
|
|
62
|
+
mission_dirs=""
|
|
63
|
+
if [[ "$scope" != mission:* ]]; then
|
|
64
|
+
mission_dirs=$(find "$DOC_ROOT" -type f \( -name 'ceo.md' -o -name 'coo.md' -o -name 'cdo.md' -o -name 'cto.md' -o -name 'cqo.md' -o -name 'ops.md' \) -exec dirname {} \; 2>/dev/null | sort -u)
|
|
65
|
+
fi
|
|
61
66
|
|
|
62
67
|
if [ "$scope" = "latest-active" ]; then
|
|
63
68
|
latest_active_mission=""
|
|
@@ -83,14 +88,37 @@ EOF
|
|
|
83
88
|
mission_dirs="$latest_active_mission"
|
|
84
89
|
fi
|
|
85
90
|
|
|
91
|
+
if [[ "$scope" == mission:* ]]; then
|
|
92
|
+
mission_rel="${scope#mission:}"
|
|
93
|
+
case "$mission_rel" in ""|/*|*..*) echo "invalid mission path" >&2; exit 1 ;; esac
|
|
94
|
+
mission_dirs="$(cd "$DOC_ROOT/$mission_rel" 2>/dev/null && pwd -P)" || exit 1
|
|
95
|
+
case "$mission_dirs/" in "$(cd "$DOC_ROOT" && pwd -P)/"*) ;; *) exit 1 ;; esac
|
|
96
|
+
[ -f "$mission_dirs/mission-state.json" ] || exit 1
|
|
97
|
+
fi
|
|
98
|
+
|
|
86
99
|
while IFS= read -r mission_dir; do
|
|
87
100
|
[ -n "$mission_dir" ] || continue
|
|
88
101
|
[ -d "$mission_dir" ] || continue
|
|
89
102
|
mission_name="${mission_dir#"$DOC_ROOT"/}"
|
|
103
|
+
tier=2
|
|
104
|
+
if [ -f "$mission_dir/mission-state.json" ]; then
|
|
105
|
+
tier=$(jq -r 'def r: if . == "S" then 0 elif . == "M" then 1 else 2 end;
|
|
106
|
+
[.tier, (.tier_history[]? | .from, .to)] | map(r) | max' "$mission_dir/mission-state.json")
|
|
107
|
+
fi
|
|
108
|
+
if [ -f "$PROJECT_ROOT/.harness/config.json" ]; then
|
|
109
|
+
enabled=$(jq -r 'if .behavior.mission_tiers == null then true else .behavior.mission_tiers end' "$PROJECT_ROOT/.harness/config.json")
|
|
110
|
+
[ "$enabled" != false ] || tier=2
|
|
111
|
+
fi
|
|
90
112
|
|
|
91
113
|
for role in ceo coo cdo cto cqo ops; do
|
|
92
114
|
role_path="$mission_dir/$role.md"
|
|
93
115
|
[ -s "$role_path" ] || continue
|
|
116
|
+
if [[ "$role" == coo || "$role" == cdo || "$role" == ops ]] && has_section "$role_path" "Not Applicable"; then
|
|
117
|
+
continue
|
|
118
|
+
fi
|
|
119
|
+
if [ "$tier" -lt 2 ] && has_section "$role_path" "Lessons"; then
|
|
120
|
+
continue
|
|
121
|
+
fi
|
|
94
122
|
if ! has_section "$role_path" "Lessons Preflight"; then
|
|
95
123
|
violations+=("$mission_name:$role.md-missing-lessons-preflight")
|
|
96
124
|
fi
|
|
@@ -116,12 +144,13 @@ if [ "$mode" = "json" ]; then
|
|
|
116
144
|
| {ok:false, violations:.}
|
|
117
145
|
'
|
|
118
146
|
else
|
|
119
|
-
echo "Lessons-before-plan violation (AGENTS.md Hard Rule 20):"
|
|
147
|
+
echo "Lessons-before-plan violation (AGENTS.md Hard Rule 20; unknown tier → L):"
|
|
120
148
|
for violation in "${violations[@]}"; do
|
|
121
149
|
mission="${violation%%:*}"
|
|
122
150
|
docs="${violation#*:}"
|
|
123
151
|
echo "- mission: $mission"
|
|
124
152
|
echo " issue: $docs"
|
|
153
|
+
echo " S/M role documents may instead use ## Lessons (Preflight/Fired)."
|
|
125
154
|
echo " required: '## Lessons Preflight' (which convention/gotcha items apply, and why)"
|
|
126
155
|
echo " '## Lessons Tally' (one line: which fired; '0 fired' is valid and must be stated)"
|
|
127
156
|
done
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pass variable names, never secret values, on the command line. Do not use bash -x.
|
|
3
|
+
set -o pipefail
|
|
4
|
+
fail() { echo "[secret-scan] $*" >&2; exit 2; }
|
|
5
|
+
VALS=()
|
|
6
|
+
while [ "$#" -gt 0 ] && [ "$1" != -- ]; do
|
|
7
|
+
[[ "$1" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || fail "invalid variable name"
|
|
8
|
+
[ -n "${!1}" ] || fail "unset or empty variable: $1"
|
|
9
|
+
VALS+=("${!1}")
|
|
10
|
+
shift
|
|
11
|
+
done
|
|
12
|
+
[ "${#VALS[@]}" -gt 0 ] && [ "${1:-}" = -- ] || fail "expected VAR_NAME... -- PATH..."
|
|
13
|
+
shift
|
|
14
|
+
[ "$#" -gt 0 ] || fail "no paths"
|
|
15
|
+
for path in "$@"; do [ -e "$path" ] || fail "missing path"; done
|
|
16
|
+
OUT=$(grep -rlF -f <(printf '%s\n' "${VALS[@]}") -- "$@")
|
|
17
|
+
RC=$?
|
|
18
|
+
COUNT=0
|
|
19
|
+
if [ -n "$OUT" ]; then
|
|
20
|
+
while IFS= read -r path; do
|
|
21
|
+
printf 'leak: %s\n' "$path"
|
|
22
|
+
COUNT=$((COUNT+1))
|
|
23
|
+
done <<< "$OUT"
|
|
24
|
+
fi
|
|
25
|
+
case "$RC" in
|
|
26
|
+
0) printf 'leaks=%s\n' "$COUNT"; exit 1 ;;
|
|
27
|
+
1) echo 'leaks=0'; exit 0 ;;
|
|
28
|
+
*) echo "scan incomplete (grep rc=$RC)" >&2; exit 2 ;;
|
|
29
|
+
esac
|
package/scripts/harness-stop.sh
CHANGED
|
@@ -160,10 +160,10 @@ if [ -x "$SCRIPT_DIR/harness-worker-evidence-validate.sh" ]; then
|
|
|
160
160
|
WORKER_EVIDENCE_OK=$(echo "$WORKER_EVIDENCE_JSON" | jq -r 'if has("ok") then .ok else true end' 2>/dev/null || echo true)
|
|
161
161
|
if [ "$WORKER_EVIDENCE_OK" != "true" ]; then
|
|
162
162
|
REASON=$(echo "$WORKER_EVIDENCE_JSON" | jq -r '
|
|
163
|
-
"
|
|
164
|
-
([.violations[] | "\(.mission) has
|
|
165
|
-
". harness-hiring/resource-manager로 전문 worker를 고용 또는 배정하고 .harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/{worker-name}.md 를 남긴 뒤 계속하라."
|
|
166
|
-
' 2>/dev/null || echo "
|
|
163
|
+
"등급별 worker/문서 증거 차단: " +
|
|
164
|
+
([.violations[] | "\(.mission) has evidence violations: \(.docs | join(","))"] | join("; ")) +
|
|
165
|
+
". S의 CTO/CQO만 worker 면제이며 S/M 역할 Notes는 축약 가능하다. 나머지는 harness-hiring/resource-manager로 전문 worker를 고용 또는 배정하고 .harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/{worker-name}.md 를 남긴 뒤 계속하라."
|
|
166
|
+
' 2>/dev/null || echo "등급별 worker/문서 증거 차단: worker report가 없는 active mission이 있습니다. hired worker 보고서를 먼저 생성하세요.")
|
|
167
167
|
jq -nc --arg reason "$REASON" '{decision:"block", reason:$reason}'
|
|
168
168
|
exit 0
|
|
169
169
|
fi
|
|
@@ -184,7 +184,7 @@ if [ -x "$SCRIPT_DIR/harness-lessons-gate.sh" ]; then
|
|
|
184
184
|
([.violations[] | "\(.mission) → \(.docs | join(","))"] | join("; ")) +
|
|
185
185
|
". 각 role 문서에 `## Lessons Preflight`(이 미션에 적용되는 conventions/gotchas 항목과 이유)와 " +
|
|
186
186
|
"그 아래 `## Implementation Notes` 바로 앞에 한 줄짜리 `## Lessons Tally`(실제로 발동한 항목; `0 fired` 도 유효하며 생략은 불가)를 " +
|
|
187
|
-
"추가한 뒤 계속하라. 코퍼스를 다시 요약한 별도 체크리스트 파일을 만들지 말 것 — 읽는 순서를 고치는 규칙이다."
|
|
187
|
+
"추가한 뒤 계속하라. S/M 역할 문서는 ## Lessons (Preflight/Fired) 한 섹션으로 대체 가능하다. 코퍼스를 다시 요약한 별도 체크리스트 파일을 만들지 말 것 — 읽는 순서를 고치는 규칙이다."
|
|
188
188
|
' 2>/dev/null || echo "교훈 선행 게이트(Hard Rule 20): active mission 의 role 문서에 ## Lessons Preflight / ## Lessons Tally 가 없습니다. 먼저 추가하세요.")
|
|
189
189
|
jq -nc --arg reason "$REASON" '{decision:"block", reason:$reason}'
|
|
190
190
|
exit 0
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Source identity is necessary, not sufficient: see CQO Evidence Reuse.
|
|
3
|
+
set -o pipefail
|
|
4
|
+
fail() { echo "[verify-fingerprint] $*" >&2; exit 2; }
|
|
5
|
+
CMD=fingerprint
|
|
6
|
+
ROOT=.
|
|
7
|
+
LOG=
|
|
8
|
+
INCLUDES=()
|
|
9
|
+
[ "${1:-}" != run ] || { CMD=run; shift; }
|
|
10
|
+
SEPARATOR=false
|
|
11
|
+
while [ "$#" -gt 0 ]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--root|--include|--log)
|
|
14
|
+
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "missing option value"
|
|
15
|
+
case "$1" in
|
|
16
|
+
--root) ROOT=$2 ;;
|
|
17
|
+
--include) INCLUDES+=("$2") ;;
|
|
18
|
+
--log) LOG=$2 ;;
|
|
19
|
+
esac
|
|
20
|
+
shift 2 ;;
|
|
21
|
+
--) SEPARATOR=true; shift; break ;;
|
|
22
|
+
*) fail "unknown option" ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
if [ "$CMD" = run ]; then
|
|
26
|
+
[ -n "$LOG" ] && [ "$SEPARATOR" = true ] && [ "$#" -gt 0 ] || fail "run requires --log PATH and -- CMD..."
|
|
27
|
+
else
|
|
28
|
+
[ "$SEPARATOR" = false ] && [ -z "$LOG" ] || fail "unexpected run options"
|
|
29
|
+
fi
|
|
30
|
+
cd -- "$ROOT" 2>/dev/null || fail "invalid root"
|
|
31
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || fail "not a git repository"
|
|
32
|
+
cd -- "$ROOT" || fail "invalid repository root"
|
|
33
|
+
if command -v shasum >/dev/null 2>&1; then HASH=(shasum -a 256)
|
|
34
|
+
elif command -v sha256sum >/dev/null 2>&1; then HASH=(sha256sum)
|
|
35
|
+
else fail "SHA-256 tool required"; fi
|
|
36
|
+
hash() { "${HASH[@]}" | awk '{print $1}'; }
|
|
37
|
+
PATHS=(. ':(top,exclude).harness' ':(top,exclude).myterm')
|
|
38
|
+
file_input() {
|
|
39
|
+
printf '%s\0' "$1"
|
|
40
|
+
if [ -e "$1" ] || [ -L "$1" ]; then
|
|
41
|
+
[ -f "$1" ] || return 2
|
|
42
|
+
hash < "$1" || return 2
|
|
43
|
+
else printf 'MISSING\n'; fi
|
|
44
|
+
}
|
|
45
|
+
fingerprint() {
|
|
46
|
+
{
|
|
47
|
+
git rev-parse --verify HEAD || return 2
|
|
48
|
+
git diff HEAD --binary --no-ext-diff --no-textconv -- "${PATHS[@]}" || return 2
|
|
49
|
+
git ls-files --others --exclude-standard -z -- "${PATHS[@]}" |
|
|
50
|
+
while IFS= read -r -d '' file; do file_input "$file" || exit 2; done || return 2
|
|
51
|
+
for file in "${INCLUDES[@]}"; do file_input "$file" || return 2; done
|
|
52
|
+
} | hash
|
|
53
|
+
}
|
|
54
|
+
BEFORE=$(fingerprint) || fail "cannot fingerprint inputs"
|
|
55
|
+
case "$CMD" in
|
|
56
|
+
fingerprint) printf '%s\n' "$BEFORE" ;;
|
|
57
|
+
run)
|
|
58
|
+
# ponytail: before/after misses ABA edits; wait for editors or use a fixed worktree.
|
|
59
|
+
# Relative log/include paths and the command are resolved from the repository root.
|
|
60
|
+
(umask 077; : > "$LOG") || fail "cannot open log"
|
|
61
|
+
"$@" > "$LOG" 2>&1
|
|
62
|
+
RC=$?
|
|
63
|
+
AFTER=$(fingerprint) || fail "cannot fingerprint inputs after run"
|
|
64
|
+
[ "$BEFORE" = "$AFTER" ] || { echo 'tree changed during run' >&2; exit 3; }
|
|
65
|
+
printf 'Baseline: cmd='
|
|
66
|
+
printf '%q ' "$@"
|
|
67
|
+
printf 'exit=%s log=%q fingerprint=%s includes=' "$RC" "$LOG" "$BEFORE"
|
|
68
|
+
printf '%q ' "${INCLUDES[@]}"
|
|
69
|
+
printf '\n'
|
|
70
|
+
exit "$RC" ;;
|
|
71
|
+
esac
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-worker-evidence-validate.sh —
|
|
2
|
+
# harness-worker-evidence-validate.sh — enforce tier-scoped role and worker evidence
|
|
3
|
+
# Usage: <root> [text|json|direct-work-sha] [all|latest-active|mission:<rel>]
|
|
3
4
|
set -euo pipefail
|
|
4
5
|
|
|
5
6
|
PROJECT_ROOT="${1:-.}"
|
|
6
7
|
DOC_ROOT="$PROJECT_ROOT/.harness/documents"
|
|
7
8
|
|
|
8
|
-
[ -d "$DOC_ROOT" ] || exit 0
|
|
9
|
-
|
|
10
9
|
mode="${2:-text}"
|
|
11
10
|
scope="${3:-all}"
|
|
11
|
+
if [ ! -d "$DOC_ROOT" ]; then
|
|
12
|
+
[[ "$scope" != mission:* && "$mode" != direct-work-sha ]] || exit 1
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
12
15
|
violations=()
|
|
13
16
|
|
|
14
17
|
# Section-scoped reading (conventions/shared.md): match `^>?\s*#{1,6}` and take
|
|
@@ -28,6 +31,29 @@ has_implementation_notes() {
|
|
|
28
31
|
grep -Eq "${HEADING3}Open Questions[[:space:]]*$" "$file"
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
has_role_notes() {
|
|
35
|
+
if [ "$tier" -lt 2 ]; then
|
|
36
|
+
grep -Eq "${HEADING2}Implementation Notes[[:space:]]*$" "$1"
|
|
37
|
+
else
|
|
38
|
+
has_implementation_notes "$1"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Preserve the exact body bytes, including trailing spaces and final newline.
|
|
43
|
+
direct_work_sha() {
|
|
44
|
+
node - "$1" <<'JS'
|
|
45
|
+
const fs = require('fs'), crypto = require('crypto');
|
|
46
|
+
const text = fs.readFileSync(process.argv[2], 'utf8');
|
|
47
|
+
const heading = /^[ \t]*>?[ \t]*##[ \t]+Direct Work[ \t]*\r?$/m.exec(text);
|
|
48
|
+
if (!heading) process.exit(1);
|
|
49
|
+
let start = heading.index + heading[0].length;
|
|
50
|
+
if (text[start] === '\n') start++;
|
|
51
|
+
const rest = text.slice(start);
|
|
52
|
+
const next = /^[ \t]*>?[ \t]*#/m.exec(rest);
|
|
53
|
+
process.stdout.write(crypto.createHash('sha256').update(rest.slice(0, next ? next.index : rest.length)).digest('hex') + '\n');
|
|
54
|
+
JS
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
has_worker_report() {
|
|
32
58
|
local mission_dir="$1"
|
|
33
59
|
local owner="${2:-}"
|
|
@@ -43,7 +69,10 @@ has_legacy_flat_worker_report() {
|
|
|
43
69
|
find "$mission_dir/workers" -maxdepth 1 -type f -name '*.md' 2>/dev/null | grep -q .
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
mission_dirs
|
|
72
|
+
mission_dirs=""
|
|
73
|
+
if [[ "$scope" != mission:* ]]; then
|
|
74
|
+
mission_dirs=$(find "$DOC_ROOT" -type f \( -name 'ceo.md' -o -name 'coo.md' -o -name 'cdo.md' -o -name 'cto.md' -o -name 'cqo.md' -o -name 'ops.md' \) -exec dirname {} \; 2>/dev/null | sort -u)
|
|
75
|
+
fi
|
|
47
76
|
|
|
48
77
|
mission_mtime() {
|
|
49
78
|
stat -f '%m' "$1" 2>/dev/null || stat -c '%Y' "$1" 2>/dev/null || echo 0
|
|
@@ -79,13 +108,36 @@ EOF
|
|
|
79
108
|
mission_dirs="$latest_active_mission"
|
|
80
109
|
fi
|
|
81
110
|
|
|
111
|
+
if [[ "$scope" == mission:* ]]; then
|
|
112
|
+
mission_rel="${scope#mission:}"
|
|
113
|
+
case "$mission_rel" in ""|/*|*..*) echo "invalid mission path" >&2; exit 1 ;; esac
|
|
114
|
+
mission_dirs="$(cd "$DOC_ROOT/$mission_rel" 2>/dev/null && pwd -P)" || exit 1
|
|
115
|
+
case "$mission_dirs/" in "$(cd "$DOC_ROOT" && pwd -P)/"*) ;; *) exit 1 ;; esac
|
|
116
|
+
[ -f "$mission_dirs/mission-state.json" ] || exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if [ "$mode" = direct-work-sha ]; then
|
|
120
|
+
[[ "$scope" == mission:* ]] || exit 1
|
|
121
|
+
direct_work_sha "$mission_dirs/cto.md"
|
|
122
|
+
exit $?
|
|
123
|
+
fi
|
|
124
|
+
|
|
82
125
|
while IFS= read -r mission_dir; do
|
|
83
126
|
[ -n "$mission_dir" ] || continue
|
|
84
127
|
[ -d "$mission_dir" ] || continue
|
|
85
128
|
mission_name="${mission_dir#$DOC_ROOT/}"
|
|
129
|
+
tier=2
|
|
130
|
+
if [ -f "$mission_dir/mission-state.json" ]; then
|
|
131
|
+
tier=$(jq -r 'def r: if . == "S" then 0 elif . == "M" then 1 else 2 end;
|
|
132
|
+
[.tier, (.tier_history[]? | .from, .to)] | map(r) | max' "$mission_dir/mission-state.json")
|
|
133
|
+
fi
|
|
134
|
+
if [ -f "$PROJECT_ROOT/.harness/config.json" ]; then
|
|
135
|
+
enabled=$(jq -r 'if .behavior.mission_tiers == null then true else .behavior.mission_tiers end' "$PROJECT_ROOT/.harness/config.json")
|
|
136
|
+
[ "$enabled" != false ] || tier=2
|
|
137
|
+
fi
|
|
86
138
|
|
|
87
139
|
ceo_path="$mission_dir/ceo.md"
|
|
88
|
-
if [ -s "$ceo_path" ] && !
|
|
140
|
+
if [ -s "$ceo_path" ] && ! has_role_notes "$ceo_path"; then
|
|
89
141
|
violations+=("$mission_name:ceo.md-missing-implementation-notes")
|
|
90
142
|
fi
|
|
91
143
|
|
|
@@ -96,10 +148,41 @@ while IFS= read -r mission_dir; do
|
|
|
96
148
|
for cxx in coo cdo cto cqo ops; do
|
|
97
149
|
cxx_path="$mission_dir/$cxx.md"
|
|
98
150
|
[ -s "$cxx_path" ] || continue
|
|
99
|
-
|
|
151
|
+
# Summoned but nothing to do: a one-reason N/A report completes the role.
|
|
152
|
+
if [[ "$cxx" == coo || "$cxx" == cdo || "$cxx" == ops ]] && grep -Eq "${HEADING2}Not Applicable[[:space:]]*$" "$cxx_path"; then
|
|
153
|
+
continue
|
|
154
|
+
fi
|
|
155
|
+
if ! has_role_notes "$cxx_path"; then
|
|
100
156
|
violations+=("$mission_name:$cxx.md-missing-implementation-notes")
|
|
101
157
|
fi
|
|
102
|
-
|
|
158
|
+
exempt=false
|
|
159
|
+
if [ "$tier" -eq 0 ] && [[ "$cxx" == cto || "$cxx" == cqo ]]; then
|
|
160
|
+
exempt=true
|
|
161
|
+
elif [ "$tier" -lt 2 ] && [ "$cxx" = ops ]; then
|
|
162
|
+
# S/M OPS observes directly; workers only for recovery or monitoring setup.
|
|
163
|
+
exempt=true
|
|
164
|
+
elif [ "$cxx" = cto ] && [ -f "$mission_dir/mission-state.json" ] && jq -e 'any(.tier_history[]?; .from == "S" and (.to == "M" or .to == "L"))' "$mission_dir/mission-state.json" >/dev/null; then
|
|
165
|
+
frozen=$(jq -r '[.tier_history[]? | select(.from == "S" and (.to == "M" or .to == "L"))][0].direct_work_sha256 // empty' "$mission_dir/mission-state.json")
|
|
166
|
+
if [ -n "$frozen" ]; then
|
|
167
|
+
current=$(direct_work_sha "$cxx_path") || current=""
|
|
168
|
+
if [ "$current" != "$frozen" ]; then
|
|
169
|
+
violations+=("$mission_name:cto-direct-work-modified-after-upgrade")
|
|
170
|
+
else
|
|
171
|
+
post=$(awk '
|
|
172
|
+
/^[[:space:]]*>?[[:space:]]*##[[:space:]]+Post-Upgrade Work[[:space:]]*$/ { inb=1; next }
|
|
173
|
+
inb && /^[[:space:]]*>?[[:space:]]*#/ { exit }
|
|
174
|
+
inb && /[^[:space:]]/ { sub(/^[[:space:]]*/, ""); print; exit }
|
|
175
|
+
' "$cxx_path")
|
|
176
|
+
# ponytail: trust the declaration; add git-diff enforcement if abuse is observed.
|
|
177
|
+
if [[ "$post" == none* ]] || { [ -n "$post" ] && has_worker_report "$mission_dir" cto; }; then
|
|
178
|
+
exempt=true
|
|
179
|
+
else
|
|
180
|
+
violations+=("$mission_name:cto-post-upgrade-work-without-worker")
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
if [ "$exempt" != true ] && ! has_worker_report "$mission_dir" "$cxx"; then
|
|
103
186
|
violations+=("$mission_name:$cxx.md")
|
|
104
187
|
fi
|
|
105
188
|
workers_dir="$mission_dir/$cxx/workers"
|
|
@@ -113,7 +196,7 @@ while IFS= read -r mission_dir; do
|
|
|
113
196
|
done
|
|
114
197
|
|
|
115
198
|
cqo_path="$mission_dir/cqo.md"
|
|
116
|
-
if [ -s "$cqo_path" ] && grep -Eq '\b(ACCEPTED|REJECTED|PASS|FAIL)\b' "$cqo_path" && ! has_worker_report "$mission_dir" "cqo"; then
|
|
199
|
+
if [ "$tier" -ne 0 ] && [ -s "$cqo_path" ] && grep -Eq '\b(ACCEPTED|REJECTED|PASS|FAIL)\b' "$cqo_path" && ! has_worker_report "$mission_dir" "cqo"; then
|
|
117
200
|
violations+=("$mission_name:cqo-verdict-without-evaluator")
|
|
118
201
|
fi
|
|
119
202
|
done <<EOF
|
|
@@ -131,6 +214,10 @@ if [ -f "$PROGRESS_FILE" ]; then
|
|
|
131
214
|
report_abs="$wreport"
|
|
132
215
|
case "$wreport" in /*) ;; *) report_abs="$PROJECT_ROOT/$wreport" ;; esac
|
|
133
216
|
[ -f "$report_abs" ] || continue
|
|
217
|
+
if [[ "$scope" == mission:* ]]; then
|
|
218
|
+
report_abs="$(cd "$(dirname "$report_abs")" && pwd -P)/$(basename "$report_abs")"
|
|
219
|
+
case "$report_abs" in "$mission_dirs/"*) ;; *) continue ;; esac
|
|
220
|
+
fi
|
|
134
221
|
# Section-scoped reading: the Status body may be quoted or indented.
|
|
135
222
|
if awk '
|
|
136
223
|
/^[[:space:]]*>?[[:space:]]*##[[:space:]]+Status[[:space:]]*$/ { inb=1; next }
|
|
@@ -160,7 +247,7 @@ if [ "$mode" = "json" ]; then
|
|
|
160
247
|
| {ok:false, violations:.}
|
|
161
248
|
'
|
|
162
249
|
else
|
|
163
|
-
echo "CXX worker evidence violation:"
|
|
250
|
+
echo "CXX worker evidence violation (unknown tier → L):"
|
|
164
251
|
for violation in "${violations[@]}"; do
|
|
165
252
|
mission="${violation%%:*}"
|
|
166
253
|
docs="${violation#*:}"
|