@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.
@@ -10,25 +10,27 @@ Route the Owner request to the installed `harness-ceo` agent/skill as an additio
10
10
  Codex adapter:
11
11
  - If Codex does not auto-load `harness-ceo`, manually read `.codex/skills/harness-ceo/SKILL.md` and follow it.
12
12
  - Absence of `.codex/agents/` is not a failure. `.codex/skills/**/SKILL.md` is the Codex runtime protocol.
13
- - For CXX "fresh session context", use role-scoped context: read the CXX skill, active mission files, and required conventions/gotchas before writing that role's `{cxx}.md`.
13
+ - Prefer a genuinely separate CQO session (Claude Agent, Codex sub-agent, or `codex exec`) from implementation. Reading another role skill in the same session is only a role switch, not independent verification. If separate execution is unavailable, record `Verification Session: same-session` and disclose the limitation to Owner; the completion gate warns but permits it.
14
+
15
+ At intake, classify and record `tier` (S/M/L) using the CEO Mission Tier criteria; record the rationale in `ceo.md`. Preserve the highest historical tier when requirements change.
14
16
 
15
17
  Required flow:
16
18
  1. Locate the active goal document root under `.harness/documents/{goal_name}/`. If no active goal exists, CEO must create/select the best matching goal record from available mission context or mark the submission blocked with the recommended default; do not ask the Owner an open-ended setup question.
17
19
  2. If another child mission under the goal is active, update its `mission-state.json` to `closed`, `cancelled`, or `superseded` with `active:false` before starting this submission.
18
20
  3. Create a submission record under `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/`.
19
- 4. Write `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/mission-state.json` with `{"lifecycle":"active","active":true}`.
21
+ 4. Write `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/mission-state.json` with `{"lifecycle":"active","active":true,"tier":"S"}` (replace S with the classified tier).
20
22
  5. Record CEO decisions in `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/ceo.md`.
21
23
  6. CEO routes only to the CXX agents needed for this additional requirement. CEO must not ask the Owner which CXX or worker path to choose.
22
24
  7. CXX roles must start in a fresh session context. Do not let the default model impersonate a missing CXX or worker.
23
- 8. CXX roles must not directly execute specialist deliverables. They must use hired workers for research, planning, design, implementation, QA, ops checks, and documentation.
25
+ 8. At effective tier M/L, and for COO/CDO deliverables at every tier, delegate specialist execution to hired workers. At S, CTO implements directly and CQO directly runs verification in a separate session; no CTO/CQO hiring is needed. At S/M, OPS observes directly. CQO starts only after CTO's CQO Handoff — never in parallel. A CXX writes a document only when summoned; a summoned COO/CDO/OPS with no work writes only `## Not Applicable` and one reason.
24
26
  9. If a required worker is missing, the responsible CXX must invoke the installed `harness-hiring` skill before assigning the work.
25
27
  10. CXX must manage conventions for new requirements. Durable changes in behavior, architecture, UI, process, or policy must be reflected in `.harness/conventions/` when accepted.
26
- 11. CEO must require a Worker Evidence Manifest and worker report paths under `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/{owning-cxx}/workers/` before accepting CXX completion.
27
- 12. When the submission is accepted, cancelled, superseded, blocked, or closed, update `mission-state.json` to `complete`, `cancelled`, `superseded`, `blocked`, or `closed` and set `active:false`. Then fire the matching runtime transition as the literal final action of the turn so the terminal document state matches the `progress.json` flag the harness reads: finished/closed → `bash scripts/harness-company-complete.sh . submission-complete`; external-authority block → `bash scripts/harness-company-block.sh . "<exact missing authority>"`. This clears `progress.json` out of `running` so the autonomous loop stops cleanly and the dashboard shows idle/done.
28
+ 11. For roles requiring workers, CEO must require a Worker Evidence Manifest and worker report paths under `.harness/documents/{goal_name}/submission-{submission_index}-{submission_name}/{owning-cxx}/workers/` before accepting CXX completion.
29
+ 12. For acceptance, leave the mission active and run `bash scripts/harness-company-complete.sh . <reason> <mission-rel>` as the final action; the script checks evidence before writing `complete` and `active:false`. On refusal, keep working. For termination without acceptance, first write lifecycle `cancelled`, `superseded`, or `closed` and `active:false`, then call the same explicit transition. `closed` means ended without acceptance; disclose “미수락 종료” in the Owner report and never archive without PASS. For an external-authority block, record `blocked`/`active:false` and run `bash scripts/harness-company-block.sh . "<exact missing authority>"`. `<mission-rel>` is the path relative to `.harness/documents/`.
28
30
  13. Do not invoke internal roles through slash commands; commands are Owner entrypoints only.
29
31
  14. Do not ask the Owner whether to continue, hire workers, choose internal options, or start the next step. If CEO cannot decide alone, convene the relevant CXX agents and decide from their written recommendations. CEO may approve reversible routine operations such as local cron/launchd/wake automation, dashboard refresh, monitoring cadence, Telegram briefing format using existing credentials, and mission consolidation/supersede cleanup. Stop only for external authority such as new credentials/secrets, payment approval, legal/business acceptance, unavailable production access, destructive data action, or direct conflict with stated Owner direction.
30
32
 
31
- Lessons before plan (AGENTS.md Hard Rule 20): CEO and every CXX read `.harness/conventions/{shared,role}.md` and `.harness/gotchas/{shared,role}.md`, follow only the topic links those files name, and write `## Lessons Preflight` **before** the first edit, the first measurement, and the first worker brief. Every role document and worker report closes with a one-line `## Lessons Tally` immediately above `## Implementation Notes` — `0 fired` is a valid tally and must be stated, not omitted. Requirements a CXX must satisfy that its workers must also satisfy go into the worker brief **verbatim**. The Stop hook enforces this; it is not advisory.
33
+ Lessons before plan: read relevant shared/role conventions and gotchas before edits, measurements, or briefs. At effective tier S/M, role documents may replace Lessons Preflight + Lessons Tally with `## Lessons` containing `Preflight: <applicable items and why>` (before work) and `Fired: <items or 0 fired>` (at completion). `## Implementation Notes` may contain concise bullets. At L, retain Lessons Preflight, Lessons Tally, and all four Implementation Notes subsections. All worker reports retain the full seeded format at every tier. **Worker brief:** name the seeded report path and instruct the worker to fill its existing sections incrementally. Do not copy the report skeleton, Tally, or Notes block into the brief. Continue to pass relevant corpus links and copy behavioral requirements absent from the seed (including the browser-automation clause) verbatim.
32
34
 
33
35
  Note: `/submission` is not a new company goal and not an emergency fix. It is an additional requirement while pursuing the active goal. It belongs under that goal in history.
34
36
 
@@ -18,3 +18,5 @@ Only company-level roles are provided by default:
18
18
  Topic-specific convention files may use descriptive names such as `i18n-locale.md`.
19
19
 
20
20
  CXX files such as `cto.md` and `cqo.md` act as lazy-loading indexes. Add links there when a topic file applies to that CXX.
21
+
22
+ **Declare the audience.** Every convention names each role that must be able to *find* it — `<!-- roles: cto, cqo -->` at the top of a topic file, or `- **Roles**: cto, cqo` inside an index entry — and is linked from each of those roles' index files, not only its author's. Lazy loading is a promise about reachability: an entry filed only under its author is indexed but invisible to the readers it names. Verify with `bash scripts/harness-corpus-reachability.sh . text` (`--fix` adds the missing links). Entries in `shared.md` need no cross-linking; every role reads it.
@@ -24,6 +24,16 @@
24
24
  - CXX decisions use `{cxx}.md`.
25
25
  - Worker reports use `{owning-cxx}/workers/{worker-name}.md`.
26
26
 
27
+ ## Declared Audience
28
+
29
+ Every convention and gotcha declares which roles must be able to **find** it, and is linked from each of those roles' index files.
30
+
31
+ - Topic file: `<!-- roles: cto, cqo -->` near the top.
32
+ - Entry inside an index file: `- **Roles**: cto, cqo`.
33
+ - Registration is complete only when `bash scripts/harness-corpus-reachability.sh . text` passes. `--fix` adds the missing links.
34
+
35
+ Lazy loading tells a reader to consult `{shared, own-role}` and nothing else. That is a **promise about reachability**: where the promise is not kept, the rule stops narrowing the search and starts hiding the entry. An agent following the reading rule exactly will never see an item filed only under someone else's index.
36
+
27
37
  ## Section-Scoped Reading
28
38
 
29
39
  Any reader that scans a role document, worker report, or mission record for sections — a script, a hook, an agent following a protocol — matches:
package/gotchas/README.md CHANGED
@@ -18,3 +18,5 @@ Only company-level roles are provided by default:
18
18
  Topic-specific gotcha files may use descriptive names such as `i18n-locale-hotfix.md`.
19
19
 
20
20
  CXX files such as `cto.md` and `cqo.md` act as lazy-loading indexes. Add links there when a topic file applies to that CXX.
21
+
22
+ **Declare the audience.** Every gotcha names each role that must be able to *find* it — `<!-- roles: cto, cqo -->` at the top of a topic file, or `- **Roles**: cto, cqo` inside an index entry — and is linked from each of those roles' index files, not only its author's. Lazy loading is a promise about reachability: an entry filed only under its author is indexed but invisible to the readers it names. Verify with `bash scripts/harness-corpus-reachability.sh . text` (`--fix` adds the missing links). Entries in `shared.md` need no cross-linking; every role reads it.
package/gotchas/cqo.md CHANGED
@@ -19,3 +19,14 @@ An instrument's filtering behaviour is read from source and quoted — package,
19
19
  ## Audit Questions That Offer Alternatives
20
20
 
21
21
  An audit question that offers alternatives asserts that the alternatives are exhaustive. "Is it a skip rule **or** a status-conditional format?" cannot return "neither, it is upstream" — both branches locate the rule inside our own code, so the true answer is unreachable from the question's grammar. When an audit stalls, re-ask the question without the menu.
22
+
23
+ ## A Range Is The Two Least Representative Points
24
+ <!-- roles: cqo, ops -->
25
+
26
+ Publish a summary statistic **with its `n`**, and characterise a spiky series by **percentiles, never min–max**. A range reports the two most extreme observations in the set and reads as a finding; on a spiky series it is almost always noise wearing the shape of a result.
27
+
28
+ ## Cross-role Links
29
+
30
+ Entries written under another role that name CQO as an audience.
31
+
32
+ - [Complete Against A Spec Version, Never In The Abstract](./cto.md) — verify `spec-pins.json` before PASS and before archive.
package/gotchas/cto.md CHANGED
@@ -11,3 +11,10 @@ Do not start implementation before domain, API, platform, account, and integrati
11
11
  ## Stub Report From A Worker Killed Mid-Round
12
12
 
13
13
  A report assembled at the end of a round becomes a stub when the round is cut short, and a stub halts the company. Create the worker report with every required section present **before** the worker starts, and require it to be filled in incrementally. Same failure, opposite outcome: an unseeded worker killed mid-round leaves a stub and costs a re-run; a seeded worker killed by the same limit leaves an intact partial report and costs nothing. The variable is a decision taken before the round.
14
+
15
+ ## Complete Against A Spec Version, Never In The Abstract
16
+ <!-- roles: cto, cqo -->
17
+
18
+ A category marked complete records *what it was complete against*. A spec moved `v0.7 → v0.9`, changing a response contract, while the category built against `v0.7` sat marked done — two later revisions landed silently and nothing in the harness recorded which version the work had been for.
19
+
20
+ The symptom was not an error. A lookup key stopped matching, and three map overlays were dropped as `null`: no exception, no log, just an absence. Pin version **and content hash** with `scripts/harness-spec-pin.sh` before implementation, and re-verify before completion and archive.
package/gotchas/ops.md CHANGED
@@ -19,3 +19,9 @@ Do not let CXX agents choose arbitrary ports. CEO must set `HARNESS_BASE_PORT` a
19
19
  ## Clean Log From An Unproven Instrument
20
20
 
21
21
  "Nothing bad appeared in the log" is worth nothing until a positive control proves the log could have shown it. Dev servers, log middleware, proxies, and test runners routinely drop successful or sub-threshold requests at a log level nobody chose deliberately, and that rule appears nowhere in the project's own code. Record the instrument — tool, log level, filter, control — in Environment Evidence, or report the observation as unverified rather than clean.
22
+
23
+ ## Cross-role Links
24
+
25
+ Entries written under another role that name OPS as an audience.
26
+
27
+ - [A Range Is The Two Least Representative Points](./cqo.md) — publish `n`, use percentiles, not min–max.
package/gotchas/shared.md CHANGED
@@ -29,3 +29,19 @@ A reader anchored on `^#` misses the same heading written as `> ## …`, which i
29
29
  ## Rules Stated One Layer Above The Executing Layer
30
30
 
31
31
  A requirement placed on a CXX that its workers must also satisfy does not reach the workers unless it is inserted **verbatim** into the worker brief. A rule stated one layer above the layer that executes it does not apply, and the layer below cannot infer a rule it was never given.
32
+
33
+ ## The Author Files Under Itself
34
+ <!-- roles: ceo, coo, cdo, cto, cqo, ops -->
35
+
36
+ The recurring way an indexed entry becomes unreachable: whoever wrote it filed it under their own role index and nowhere else. Registration *feels* complete — the entry exists, it is indexed, it is linked. It is just not where its declared readers are told to look.
37
+
38
+ Measured on a live corpus: 69 items, **10 unreachable role-routings, 7 of them invisible to a role the entry itself named**. The clearest case was an entry whose own text called two others "the same family" — both siblings were already reachable from the index it was missing from. An inconsistency, not a decision.
39
+
40
+ Declare the audience at registration and let `scripts/harness-corpus-reachability.sh` link it. Reading is not reaching.
41
+
42
+ ## A Conclusion Not Written Is Not Held
43
+ <!-- roles: ceo, coo, cdo, cto, cqo, ops -->
44
+
45
+ A conclusion a session holds but has not written into its role document and the runtime state file is not held by the company. Reconcile before reporting; strike and correct in place, never delete.
46
+
47
+ The cheap version: a deliverable table that contradicts three messages already sent, and a line still requesting work a peer has already delivered. The expensive version was measured — a required step completed, reported, and accepted, that never reached the state file, so the orchestration loop went on trying to spawn the finished step **70 times**. "Write your conclusions down" reads as tidiness until it reads as seventy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "7.1.56",
3
+ "version": "7.1.58",
4
4
  "description": "Company-style AI agent harness for Claude and Codex. Installs commands, CXX agents, skills, HR-Resource hiring pool, and project-local .harness runtime state.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -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 or rewrite mission documents.
9
+ # runtime state and mission lifecycle; it does not archive mission documents.
10
10
 
11
11
  set -uo pipefail
12
12
 
@@ -16,6 +16,7 @@ REASON="${2:-mission-complete}"
16
16
 
17
17
  PROGRESS="$PROJECT_ROOT/.harness/progress.json"
18
18
  TODOS="$PROJECT_ROOT/.harness/todos/state.json"
19
+ DOCS="$PROJECT_ROOT/.harness/documents"
19
20
  [ -f "$PROGRESS" ] || {
20
21
  echo "[company-complete] not found: $PROGRESS" >&2
21
22
  exit 1
@@ -25,30 +26,6 @@ command -v jq >/dev/null 2>&1 || {
25
26
  exit 1
26
27
  }
27
28
 
28
- # Lessons-before-plan gate (AGENTS.md Hard Rule 20). The Stop hook cannot see
29
- # this path: once this script sets conductor.state=completed, harness-stop.sh
30
- # short-circuits at the top, so a mission could complete having never read the
31
- # corpus simply by firing the transition. Refuse the terminal transition here —
32
- # this is the transition P6 names, and it is the last point at which refusing
33
- # still means anything.
34
- #
35
- # Safe against deadlock: the gate is scoped to the latest ACTIVE mission, so the
36
- # stop-hook backstop (which fires only when no mission is active) always passes.
37
- # Opt out per project with .harness/config.json behavior.lessons_gate=false, or
38
- # override a single call with HARNESS_SKIP_LESSONS_GATE=1.
39
- if [ "${HARNESS_SKIP_LESSONS_GATE:-0}" != "1" ] && [ -x "$SCRIPT_DIR/harness-lessons-gate.sh" ]; then
40
- if ! gate_out="$(bash "$SCRIPT_DIR/harness-lessons-gate.sh" "$PROJECT_ROOT" text latest-active 2>/dev/null)"; then
41
- {
42
- echo "[company-complete] REFUSED: the active mission has not recorded what it read before planning."
43
- echo "$gate_out"
44
- echo " Add the two sections to each role document, then re-run this transition."
45
- echo " (override: HARNESS_SKIP_LESSONS_GATE=1, or behavior.lessons_gate=false in .harness/config.json)"
46
- } >&2
47
- 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
48
- exit 1
49
- fi
50
- fi
51
-
52
29
  state_mtime() {
53
30
  stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0
54
31
  }
@@ -72,6 +49,117 @@ pick_transition_mission_state() {
72
49
  [ -n "$best" ] && printf '%s\n' "$best"
73
50
  }
74
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
+
145
+ # Spec pins are verified against the mission this transition would close. This
146
+ # sits BEFORE the runtime transition on purpose: a refusal that runs after
147
+ # progress.json is already `completed` has refused nothing.
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"
150
+ if [ -n "$pin_target" ]; then
151
+ mission_rel="${pin_target#"$DOCS"/}"; mission_rel="${mission_rel%/mission-state.json}"
152
+ if [ "$#" -ge 3 ]; then mission_rel="$3"; fi
153
+ if ! pin_out="$(bash "$SCRIPT_DIR/harness-spec-pin.sh" "$PROJECT_ROOT" "$mission_rel" verify text 2>/dev/null)"; then
154
+ refuse "spec-pin drift: $pin_out"
155
+ fi
156
+ fi
157
+ fi
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
+
75
163
  if ! bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
76
164
  '.company_state.state = "idle" |
77
165
  .company_state.active_workers = 0 |
@@ -109,13 +197,11 @@ if [ -f "$TODOS" ]; then
109
197
  ' "$TODOS" > "$tmp" && mv "$tmp" "$TODOS"
110
198
  fi
111
199
 
112
- DOCS="$PROJECT_ROOT/.harness/documents"
113
200
  if [ -d "$DOCS" ]; then
114
- target_state="$(pick_transition_mission_state)"
115
201
  if [ -n "$target_state" ]; then
116
202
  lifecycle="$(jq -r '.lifecycle // .status // "unknown"' "$target_state" 2>/dev/null || echo unknown)"
117
203
  case "$lifecycle" in
118
- closed|cancelled|superseded|complete|completed) ;;
204
+ closed|cancelled|superseded) ;;
119
205
  *)
120
206
  tmp="$(mktemp)"
121
207
  jq '.lifecycle = "complete" | .active = false | .completed_at = (now | todate) | del(.blocked_reason) | del(.blocked_at)' "$target_state" > "$tmp" && mv "$tmp" "$target_state"
@@ -0,0 +1,128 @@
1
+ #!/bin/bash
2
+ # harness-corpus-reachability.sh — AGENTS.md Hard Rule 11 (reachability clause).
3
+ #
4
+ # Lazy loading tells a reader to consult only {shared, own-role}. That is a
5
+ # PROMISE ABOUT REACHABILITY. Where it is not kept, the rule does not narrow the
6
+ # search — it hides the entry: an item can name a role as its audience, be
7
+ # indexed, and still be invisible to that reader by the path the rule tells it
8
+ # to use. Measured on a live corpus: 69 items, 10 unreachable role-routings,
9
+ # 7 of them invisible to a role the item itself named.
10
+ #
11
+ # The mechanism is mundane and recurs on any install: THE AUTHOR FILES UNDER
12
+ # ITSELF. Registration feels complete because the entry is indexed — just not
13
+ # where its declared readers look.
14
+ #
15
+ # Declaring an audience (either form):
16
+ # file-level (topic files): <!-- roles: cto, cqo -->
17
+ # entry-level (index files): - **Roles**: cto, cqo
18
+ #
19
+ # Usage:
20
+ # harness-corpus-reachability.sh <project-root> [text|json] [--fix]
21
+ set -uo pipefail
22
+
23
+ PROJECT_ROOT="${1:-.}"
24
+ MODE="${2:-text}"
25
+ FIX="${3:-}"
26
+
27
+ ROLES="ceo coo cdo cto cqo ops hiring resource-manager brick-office shared"
28
+ violations=()
29
+ fixed=0
30
+
31
+ is_index_file() {
32
+ local base="$1"
33
+ case " $ROLES " in *" ${base%.md} "*) return 0 ;; esac
34
+ return 1
35
+ }
36
+
37
+ # Roles this file/entry names as an audience, whitespace-separated, deduped.
38
+ #
39
+ # A declaration must OWN its line. Anchoring here is what keeps the audit from
40
+ # reading its own documentation as data: prose that shows the syntax inline
41
+ # (`<!-- roles: cto, cqo -->` inside backticks) is an example, not a routing,
42
+ # and counting it makes the audit report holes that do not exist.
43
+ declared_roles() {
44
+ {
45
+ grep -oiE '^[[:space:]]*<!--[[:space:]]*roles?:[^>]*-->' "$1" 2>/dev/null | sed -E 's/^[[:space:]]*<!--[[:space:]]*[Rr]oles?:[[:space:]]*//; s/[[:space:]]*-->//'
46
+ grep -oiE '^[[:space:]]*[-*][[:space:]]*\*\*Roles?\*\*:.*' "$1" 2>/dev/null | sed -E 's/.*\*\*[Rr]oles?\*\*:[[:space:]]*//'
47
+ } | tr ',' '\n' | tr -d ' \t' | grep -v '^$' | tr 'A-Z' 'a-z' | sort -u
48
+ }
49
+
50
+ # Does <index> reach <target basename>? Only a real markdown link target counts.
51
+ # A bare mention of the filename in prose is not a route a reader can follow,
52
+ # and counting it would make the audit report success on exactly the entries it
53
+ # exists to find. Matches `](./file.md`, `](file.md`, with or without an anchor.
54
+ reaches() {
55
+ local index="$1" target="$2"
56
+ [ -f "$index" ] || return 1
57
+ grep -qF -- "](./$target" "$index" || grep -qF -- "]($target" "$index"
58
+ }
59
+
60
+ audit_dir() {
61
+ local kind="$1" dir="$PROJECT_ROOT/.harness/$1"
62
+ [ -d "$dir" ] || return 0
63
+ local f base host_role roles role index
64
+ for f in "$dir"/*.md; do
65
+ [ -e "$f" ] || continue
66
+ base="$(basename "$f")"
67
+ [ "$base" = "README.md" ] && continue
68
+ # shared.md is read by every role by construction (Hard Rule 11), so an entry
69
+ # living there is already reachable to every audience it could name.
70
+ [ "$base" = "shared.md" ] && continue
71
+ host_role="${base%.md}"
72
+ roles="$(declared_roles "$f")"
73
+ [ -n "$roles" ] || continue
74
+ while IFS= read -r role; do
75
+ [ -n "$role" ] || continue
76
+ # The host file is reachable to its own role by construction.
77
+ if is_index_file "$base" && [ "$role" = "$host_role" ]; then continue; fi
78
+ case " $ROLES " in *" $role "*) ;; *) continue ;; esac
79
+ # Naming `shared` as an audience is satisfied by construction.
80
+ [ "$role" = "shared" ] && continue
81
+ index="$dir/${role}.md"
82
+ if reaches "$index" "$base"; then continue; fi
83
+ if [ "$FIX" = "--fix" ]; then
84
+ [ -f "$index" ] || printf '# %s — %s\n' "${role}" "${kind}" > "$index"
85
+ if ! grep -qF 'Cross-role Links' "$index"; then
86
+ printf '\n## Cross-role Links\n\nItems written elsewhere that name this role as an audience.\n' >> "$index"
87
+ fi
88
+ printf -- '- [%s](./%s) — declares `%s`\n' "${base%.md}" "$base" "$role" >> "$index"
89
+ fixed=$((fixed + 1))
90
+ else
91
+ violations+=("$kind/$base:$role")
92
+ fi
93
+ done <<EOF
94
+ $roles
95
+ EOF
96
+ done
97
+ }
98
+
99
+ audit_dir conventions
100
+ audit_dir gotchas
101
+
102
+ if [ "$FIX" = "--fix" ]; then
103
+ echo "[corpus-reachability] linked $fixed previously unreachable routing(s)"
104
+ exit 0
105
+ fi
106
+
107
+ if [ "${#violations[@]}" -eq 0 ]; then
108
+ [ "$MODE" = "json" ] && { jq -nc '{ok:true, unreachable:[]}' 2>/dev/null || echo '{"ok":true,"unreachable":[]}'; }
109
+ [ "$MODE" = "text" ] && echo "[corpus-reachability] all declared audiences reachable"
110
+ exit 0
111
+ fi
112
+
113
+ if [ "$MODE" = "json" ]; then
114
+ printf '%s\n' "${violations[@]}" | jq -Rcs '
115
+ split("\n")[:-1]
116
+ | map(capture("(?<item>[^:]+):(?<role>.*)"))
117
+ | {ok:false, unreachable:.}
118
+ '
119
+ else
120
+ echo "Corpus reachability violation (AGENTS.md Hard Rule 11):"
121
+ for v in "${violations[@]}"; do
122
+ echo "- item: ${v%%:*}"
123
+ echo " names role '${v#*:}' as an audience, but ${v#*:}'s index does not reach it"
124
+ done
125
+ echo " An entry is registered only when every role it names can reach it by the"
126
+ echo " path that role is told to use. Fix: harness-corpus-reachability.sh <root> text --fix"
127
+ fi
128
+ exit 1
@@ -11,7 +11,8 @@
11
11
  # --right "올바른 행동" \
12
12
  # --why "근거" \
13
13
  # --scope "적용 범위" \
14
- # --source "evaluator-functional:F-003"
14
+ # --source "evaluator-functional:F-003" \\
15
+ # --roles "cqo, cto"
15
16
  #
16
17
  # 2) 일괄 등록 (JSON stdin/파일):
17
18
  # bash harness-gotcha-register.sh <project-root> --from-json <path>
@@ -69,6 +70,7 @@ TODAY="$(date +%Y-%m-%d)"
69
70
  # ─────────────────────────────────────────
70
71
  register_one() {
71
72
  local target="$1" rule_id="$2" title="$3" wrong="$4" right="$5" why="$6" scope="$7" source="$8"
73
+ local roles="${9:-$target}"
72
74
  local file="$GOTCHAS_DIR/${target}.md"
73
75
  mkdir -p "$(dirname "$file")"
74
76
 
@@ -118,6 +120,7 @@ EOF
118
120
  {
119
121
  echo ""
120
122
  echo "### [$g_id] $title <!-- rule_id: $rule_id -->"
123
+ echo "- **Roles**: $roles"
121
124
  echo "- **Status**: unverified"
122
125
  echo "- **Date**: $TODAY"
123
126
  echo "- **Source**: $source"
@@ -132,6 +135,13 @@ EOF
132
135
 
133
136
  echo "[gotcha-register] $target: registered $g_id ($rule_id) — unverified"
134
137
 
138
+ # Hard Rule 11: an entry filed only under its author is indexed but unreachable
139
+ # to the other roles it names. Link it now, while the audience is still known.
140
+ local reach="$(dirname "$0")/harness-corpus-reachability.sh"
141
+ if [ -x "$reach" ] && [ "$roles" != "$target" ]; then
142
+ bash "$reach" "$PROJECT_ROOT" text --fix >/dev/null 2>&1 || true
143
+ fi
144
+
135
145
  # Log to progress.log if present
136
146
  local progress_log="$PROJECT_ROOT/.harness/progress.log"
137
147
  if [ -f "$progress_log" ]; then
@@ -141,7 +151,9 @@ EOF
141
151
 
142
152
  # ─────────────────────────────────────────
143
153
  # JSON 배열에서 일괄 등록
144
- # schema: [{ target, rule_id, title, wrong, right, why, scope, source }]
154
+ # schema: [{ target, rule_id, title, wrong, right, why, scope, source, roles }]
155
+ # `roles` is the comma-separated list of every role that must be able to FIND
156
+ # this entry — not only the one that wrote it (Hard Rule 11). Defaults to target.
145
157
  # ─────────────────────────────────────────
146
158
  register_from_json() {
147
159
  local json="$1"
@@ -153,7 +165,7 @@ register_from_json() {
153
165
 
154
166
  local i
155
167
  for ((i=0; i<count; i++)); do
156
- local t r ti w ri wh sc so
168
+ local t r ti w ri wh sc so rl
157
169
  t=$(echo "$json" | jq -r ".[$i].target // empty")
158
170
  r=$(echo "$json" | jq -r ".[$i].rule_id // empty")
159
171
  ti=$(echo "$json" | jq -r ".[$i].title // empty")
@@ -162,12 +174,13 @@ register_from_json() {
162
174
  wh=$(echo "$json" | jq -r ".[$i].why // empty")
163
175
  sc=$(echo "$json" | jq -r ".[$i].scope // \"항상\"")
164
176
  so=$(echo "$json" | jq -r ".[$i].source // \"evaluator:auto\"")
177
+ rl=$(echo "$json" | jq -r ".[$i].roles // empty")
165
178
 
166
179
  if [ -z "$t" ] || [ -z "$r" ] || [ -z "$ti" ]; then
167
180
  echo "[gotcha-register] skip: missing target/rule_id/title at index $i" >&2
168
181
  continue
169
182
  fi
170
- register_one "$t" "$r" "$ti" "$w" "$ri" "$wh" "$sc" "$so"
183
+ register_one "$t" "$r" "$ti" "$w" "$ri" "$wh" "$sc" "$so" "${rl:-$t}"
171
184
  done
172
185
  }
173
186
 
@@ -354,6 +367,7 @@ RIGHT=""
354
367
  WHY=""
355
368
  SCOPE="항상"
356
369
  SOURCE="evaluator:auto"
370
+ ROLES_DECL=""
357
371
  MODE="single"
358
372
  FROM_JSON=""
359
373
 
@@ -367,6 +381,7 @@ while [ $# -gt 0 ]; do
367
381
  --why) WHY="$2"; shift 2 ;;
368
382
  --scope) SCOPE="$2"; shift 2 ;;
369
383
  --source) SOURCE="$2"; shift 2 ;;
384
+ --roles) ROLES_DECL="$2"; shift 2 ;;
370
385
  --from-json) MODE="json"; FROM_JSON="$2"; shift 2 ;;
371
386
  --scan-evaluations) MODE="scan"; shift ;;
372
387
  --scan-all) MODE="scan-all"; shift ;;
@@ -380,7 +395,7 @@ case "$MODE" in
380
395
  echo "[gotcha-register] usage: --target X --rule-id Y --title Z [...]" >&2
381
396
  exit 1
382
397
  fi
383
- register_one "$TARGET" "$RULE_ID" "$TITLE" "$WRONG" "$RIGHT" "$WHY" "$SCOPE" "$SOURCE"
398
+ register_one "$TARGET" "$RULE_ID" "$TITLE" "$WRONG" "$RIGHT" "$WHY" "$SCOPE" "$SOURCE" "${ROLES_DECL:-$TARGET}"
384
399
  ;;
385
400
  json)
386
401
  if [ ! -f "$FROM_JSON" ]; then echo "[gotcha-register] file not found: $FROM_JSON" >&2; exit 1; fi