@walwal-harness/cli 7.1.56 → 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.
@@ -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=$(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)
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
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # harness-spec-pin.sh — AGENTS.md Hard Rule 4 (spec-pin clause).
3
+ #
4
+ # A category is complete AGAINST A SPEC VERSION, never in the abstract. Measured:
5
+ # a spec moved v0.7 -> v0.9, changing a response contract, while the category
6
+ # built against v0.7 sat marked complete — two later revisions had landed
7
+ # silently, and nothing in the harness recorded which version the work was for.
8
+ # The symptom was a lookup key that no longer matched: no error, no log, three
9
+ # overlays dropped as null.
10
+ #
11
+ # Usage:
12
+ # harness-spec-pin.sh <project-root> <mission-rel> add <name> <path> [version]
13
+ # harness-spec-pin.sh <project-root> <mission-rel> verify [text|json]
14
+ # harness-spec-pin.sh <project-root> <mission-rel> list
15
+ set -uo pipefail
16
+
17
+ PROJECT_ROOT="${1:-.}"
18
+ MISSION_REL="${2:-}"
19
+ CMD="${3:-verify}"
20
+ DOCS="$PROJECT_ROOT/.harness/documents"
21
+ PINS="$DOCS/$MISSION_REL/spec-pins.json"
22
+
23
+ command -v jq >/dev/null 2>&1 || { echo "[spec-pin] jq required" >&2; exit 2; }
24
+ [ -n "$MISSION_REL" ] || { echo "[spec-pin] mission path required" >&2; exit 2; }
25
+
26
+ hash_of() {
27
+ local f="$1"
28
+ [ -f "$f" ] || { printf 'MISSING'; return; }
29
+ if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$f" | awk '{print $1}'
30
+ elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$f" | awk '{print $1}'
31
+ else printf 'NOHASHER'; fi
32
+ }
33
+
34
+ case "$CMD" in
35
+ add)
36
+ name="${4:-}"; specpath="${5:-}"; version="${6:-unversioned}"
37
+ [ -n "$name" ] && [ -n "$specpath" ] || { echo "[spec-pin] usage: add <name> <path> [version]" >&2; exit 2; }
38
+ mkdir -p "$(dirname "$PINS")"
39
+ [ -f "$PINS" ] || echo '{"pins":[]}' > "$PINS"
40
+ abs="$specpath"; case "$specpath" in /*) ;; *) abs="$PROJECT_ROOT/$specpath" ;; esac
41
+ h="$(hash_of "$abs")"
42
+ tmp="$(mktemp)"
43
+ jq --arg n "$name" --arg p "$specpath" --arg v "$version" --arg h "$h" \
44
+ '.pins = ((.pins // []) | map(select(.name != $n)) + [{name:$n, path:$p, version:$v, sha256:$h, pinned_at:(now|todate)}])' \
45
+ "$PINS" > "$tmp" && mv "$tmp" "$PINS"
46
+ echo "[spec-pin] pinned $name @ $version ($h)"
47
+ ;;
48
+ list)
49
+ [ -f "$PINS" ] && jq -r '.pins[] | "- \(.name) @ \(.version) — \(.path) [\(.sha256[0:12])]"' "$PINS" || echo "(no pins)"
50
+ ;;
51
+ verify)
52
+ mode="${4:-text}"
53
+ # No pins file is not a failure: not every mission builds against an external spec.
54
+ if [ ! -f "$PINS" ]; then
55
+ [ "$mode" = "json" ] && jq -nc '{ok:true, drifted:[], pinned:0}'
56
+ exit 0
57
+ fi
58
+ drifted=()
59
+ while IFS=$'\t' read -r name specpath version want; do
60
+ [ -n "$name" ] || continue
61
+ abs="$specpath"; case "$specpath" in /*) ;; *) abs="$PROJECT_ROOT/$specpath" ;; esac
62
+ got="$(hash_of "$abs")"
63
+ [ "$got" = "NOHASHER" ] && continue
64
+ if [ "$got" != "$want" ]; then
65
+ if [ "$got" = "MISSING" ]; then drifted+=("$name|$version|spec file is gone: $specpath")
66
+ else drifted+=("$name|$version|content changed since it was pinned: $specpath"); fi
67
+ fi
68
+ done < <(jq -r '.pins[]? | [.name, .path, .version, .sha256] | @tsv' "$PINS" 2>/dev/null)
69
+ total="$(jq '[.pins[]?] | length' "$PINS" 2>/dev/null || echo 0)"
70
+ if [ "${#drifted[@]}" -eq 0 ]; then
71
+ [ "$mode" = "json" ] && jq -nc --argjson n "$total" '{ok:true, drifted:[], pinned:$n}'
72
+ [ "$mode" = "text" ] && echo "[spec-pin] $total pin(s) still match the current documents"
73
+ exit 0
74
+ fi
75
+ if [ "$mode" = "json" ]; then
76
+ printf '%s\n' "${drifted[@]}" | jq -Rcs 'split("\n")[:-1] | map(split("|") | {name:.[0], pinned_version:.[1], detail:.[2]}) | {ok:false, drifted:.}'
77
+ else
78
+ echo "Spec pin drift (AGENTS.md Hard Rule 4):"
79
+ for d in "${drifted[@]}"; do
80
+ rest="${d#*|}"
81
+ echo "- ${d%%|*} (built against ${rest%%|*})"
82
+ echo " ${d##*|}"
83
+ done
84
+ echo " This mission was built against a spec that has since moved. Re-check the"
85
+ echo " affected work before marking anything complete, then re-pin:"
86
+ echo " scripts/harness-spec-pin.sh . $MISSION_REL add <name> <path> <new-version>"
87
+ fi
88
+ exit 1
89
+ ;;
90
+ *) echo "[spec-pin] unknown command: $CMD" >&2; exit 2 ;;
91
+ esac
@@ -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
- "CXX 직접 실행 차단: " +
164
- ([.violations[] | "\(.mission) has CXX docs without worker reports: \(.docs | join(","))"] | join("; ")) +
165
- ". harness-hiring/resource-manager로 전문 worker를 고용 또는 배정하고 .harness/documents/{goal-or-child-mission}/{owning-cxx}/workers/{worker-name}.md 를 남긴 뒤 계속하라."
166
- ' 2>/dev/null || echo "CXX 직접 실행 차단: worker report가 없는 active mission이 있습니다. hired worker 보고서를 먼저 생성하세요.")
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 — block CXX direct execution without worker reports
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=$(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)
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" ] && ! has_implementation_notes "$ceo_path"; then
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
- if ! has_implementation_notes "$cxx_path"; then
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
- if ! has_worker_report "$mission_dir" "$cxx"; then
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,13 +196,43 @@ 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
120
203
  $mission_dirs
121
204
  EOF
122
205
 
206
+ # Record drift (AGENTS.md Hard Rule 12): a conclusion the session holds but has
207
+ # not written into the state file is not held by the company. Measured cost of
208
+ # the unreconciled case: a finished step the orchestration loop went on trying
209
+ # to spawn 70 times, because only the report knew it was done.
210
+ PROGRESS_FILE="$PROJECT_ROOT/.harness/progress.json"
211
+ if [ -f "$PROGRESS_FILE" ]; then
212
+ while IFS=$'\t' read -r wname wreport; do
213
+ [ -n "$wname" ] && [ -n "$wreport" ] || continue
214
+ report_abs="$wreport"
215
+ case "$wreport" in /*) ;; *) report_abs="$PROJECT_ROOT/$wreport" ;; esac
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
221
+ # Section-scoped reading: the Status body may be quoted or indented.
222
+ if awk '
223
+ /^[[:space:]]*>?[[:space:]]*##[[:space:]]+Status[[:space:]]*$/ { inb=1; next }
224
+ inb && /^[[:space:]]*>?[[:space:]]*#/ { inb=0 }
225
+ inb && /COMPLETE/ { found=1 }
226
+ END { exit(found ? 0 : 1) }
227
+ ' "$report_abs" 2>/dev/null; then
228
+ violations+=("state-file:worker-$wname-reported-COMPLETE-but-progress.json-still-running")
229
+ fi
230
+ done < <(jq -r '
231
+ [.company_state.workers[]? | select((.status // "") | test("running|busy"))
232
+ | [(.name // .worker // .feature // "unknown"), (.report // .report_path // .path // "")]]
233
+ | .[] | @tsv' "$PROGRESS_FILE" 2>/dev/null)
234
+ fi
235
+
123
236
  if [ "${#violations[@]}" -eq 0 ]; then
124
237
  if [ "$mode" = "json" ]; then
125
238
  jq -nc '{ok:true, violations:[]}'
@@ -134,7 +247,7 @@ if [ "$mode" = "json" ]; then
134
247
  | {ok:false, violations:.}
135
248
  '
136
249
  else
137
- echo "CXX worker evidence violation:"
250
+ echo "CXX worker evidence violation (unknown tier → L):"
138
251
  for violation in "${violations[@]}"; do
139
252
  mission="${violation%%:*}"
140
253
  docs="${violation#*:}"