@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.
- package/CHANGELOG.md +27 -0
- package/HR-Resource/cdo/SKILL.md +6 -0
- package/HR-Resource/ceo/SKILL.md +51 -17
- package/HR-Resource/coo/SKILL.md +6 -0
- package/HR-Resource/cqo/SKILL.md +69 -28
- package/HR-Resource/cto/SKILL.md +43 -21
- package/HR-Resource/hiring/SKILL.md +5 -21
- package/HR-Resource/ops/SKILL.md +27 -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 +44 -20
- 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/conventions/README.md +2 -0
- package/conventions/shared.md +10 -0
- package/gotchas/README.md +2 -0
- package/gotchas/cqo.md +11 -0
- package/gotchas/cto.md +7 -0
- package/gotchas/ops.md +6 -0
- package/gotchas/shared.md +16 -0
- package/package.json +1 -1
- package/scripts/harness-company-complete.sh +115 -29
- package/scripts/harness-corpus-reachability.sh +128 -0
- package/scripts/harness-gotcha-register.sh +20 -5
- package/scripts/harness-lessons-gate.sh +34 -5
- package/scripts/harness-secret-scan.sh +29 -0
- package/scripts/harness-spec-pin.sh +91 -0
- package/scripts/harness-stop.sh +5 -5
- package/scripts/harness-verify-fingerprint.sh +71 -0
- package/scripts/harness-worker-evidence-validate.sh +122 -9
package/commands/submission.md
CHANGED
|
@@ -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
|
-
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
package/conventions/README.md
CHANGED
|
@@ -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.
|
package/conventions/shared.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|