@datafog/fogclaw 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/backlog-tools.d.ts +57 -0
- package/dist/backlog-tools.d.ts.map +1 -0
- package/dist/backlog-tools.js +173 -0
- package/dist/backlog-tools.js.map +1 -0
- package/dist/backlog.d.ts +82 -0
- package/dist/backlog.d.ts.map +1 -0
- package/dist/backlog.js +169 -0
- package/dist/backlog.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -2
- package/dist/index.js.map +1 -1
- package/dist/message-sending-handler.d.ts +2 -1
- package/dist/message-sending-handler.d.ts.map +1 -1
- package/dist/message-sending-handler.js +5 -1
- package/dist/message-sending-handler.js.map +1 -1
- package/dist/tool-result-handler.d.ts +2 -1
- package/dist/tool-result-handler.d.ts.map +1 -1
- package/dist/tool-result-handler.js +5 -1
- package/dist/tool-result-handler.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/openclaw.plugin.json +11 -1
- package/package.json +7 -1
- package/.github/workflows/harness-docs.yml +0 -30
- package/AGENTS.md +0 -28
- package/docs/DATA.md +0 -28
- package/docs/DESIGN.md +0 -17
- package/docs/DOMAIN_DOCS.md +0 -30
- package/docs/FRONTEND.md +0 -24
- package/docs/OBSERVABILITY.md +0 -32
- package/docs/PLANS.md +0 -171
- package/docs/PRODUCT_SENSE.md +0 -20
- package/docs/RELIABILITY.md +0 -60
- package/docs/SECURITY.md +0 -52
- package/docs/design-docs/core-beliefs.md +0 -17
- package/docs/design-docs/index.md +0 -8
- package/docs/generated/README.md +0 -36
- package/docs/generated/memory.md +0 -1
- package/docs/plans/2026-02-16-fogclaw-design.md +0 -172
- package/docs/plans/2026-02-16-fogclaw-implementation.md +0 -1606
- package/docs/plans/README.md +0 -15
- package/docs/plans/active/2026-02-16-feat-openclaw-official-submission-plan.md +0 -386
- package/docs/plans/active/2026-02-17-feat-release-fogclaw-via-datafog-package-plan.md +0 -328
- package/docs/plans/active/2026-02-17-feat-submit-fogclaw-to-openclaw-plan.md +0 -244
- package/docs/plans/active/2026-02-17-feat-tool-result-pii-scanning-plan.md +0 -293
- package/docs/plans/tech-debt-tracker.md +0 -42
- package/docs/plugins/fogclaw.md +0 -101
- package/docs/runbooks/address-review-findings.md +0 -30
- package/docs/runbooks/ci-failures.md +0 -46
- package/docs/runbooks/code-review.md +0 -34
- package/docs/runbooks/merge-change.md +0 -28
- package/docs/runbooks/pull-request.md +0 -45
- package/docs/runbooks/record-evidence.md +0 -43
- package/docs/runbooks/reproduce-bug.md +0 -42
- package/docs/runbooks/respond-to-feedback.md +0 -42
- package/docs/runbooks/review-findings.md +0 -31
- package/docs/runbooks/submit-openclaw-plugin.md +0 -68
- package/docs/runbooks/update-agents-md.md +0 -59
- package/docs/runbooks/update-domain-docs.md +0 -42
- package/docs/runbooks/validate-current-state.md +0 -41
- package/docs/runbooks/verify-release.md +0 -69
- package/docs/specs/2026-02-16-feat-openclaw-official-submission-spec.md +0 -115
- package/docs/specs/2026-02-17-feat-outbound-message-pii-scanning-spec.md +0 -93
- package/docs/specs/2026-02-17-feat-submit-fogclaw-to-openclaw.md +0 -125
- package/docs/specs/2026-02-17-feat-tool-result-pii-scanning-spec.md +0 -122
- package/docs/specs/README.md +0 -5
- package/docs/specs/index.md +0 -8
- package/docs/spikes/README.md +0 -8
- package/fogclaw.config.example.json +0 -33
- package/scripts/ci/he-docs-config.json +0 -123
- package/scripts/ci/he-docs-drift.sh +0 -112
- package/scripts/ci/he-docs-lint.sh +0 -234
- package/scripts/ci/he-plans-lint.sh +0 -354
- package/scripts/ci/he-runbooks-lint.sh +0 -445
- package/scripts/ci/he-specs-lint.sh +0 -258
- package/scripts/ci/he-spikes-lint.sh +0 -249
- package/scripts/runbooks/select-runbooks.sh +0 -154
- package/src/config.ts +0 -183
- package/src/engines/gliner.ts +0 -240
- package/src/engines/regex.ts +0 -71
- package/src/extract.ts +0 -98
- package/src/index.ts +0 -381
- package/src/message-sending-handler.ts +0 -87
- package/src/redactor.ts +0 -51
- package/src/scanner.ts +0 -196
- package/src/tool-result-handler.ts +0 -133
- package/src/types.ts +0 -75
- package/tests/config.test.ts +0 -78
- package/tests/extract.test.ts +0 -185
- package/tests/gliner.test.ts +0 -289
- package/tests/message-sending-handler.test.ts +0 -244
- package/tests/plugin-smoke.test.ts +0 -250
- package/tests/redactor.test.ts +0 -320
- package/tests/regex.test.ts +0 -345
- package/tests/scanner.test.ts +0 -348
- package/tests/tool-result-handler.test.ts +0 -329
- package/tsconfig.json +0 -20
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# ── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
-
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
6
|
-
DEFAULT_CONFIG_PATH="scripts/ci/he-docs-config.json"
|
|
7
|
-
|
|
8
|
-
# ── Globals ──────────────────────────────────────────────────────────────────
|
|
9
|
-
ERRORS=0
|
|
10
|
-
WARNINGS=0
|
|
11
|
-
|
|
12
|
-
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
_env_flag() {
|
|
15
|
-
local name="$1"
|
|
16
|
-
local default="${2:-0}"
|
|
17
|
-
local val="${!name:-$default}"
|
|
18
|
-
[[ "$val" == "1" ]]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
_load_config() {
|
|
22
|
-
local config_path="${HARNESS_DOCS_CONFIG:-$DEFAULT_CONFIG_PATH}"
|
|
23
|
-
local path="$REPO_ROOT/$config_path"
|
|
24
|
-
if [[ ! -f "$path" ]]; then
|
|
25
|
-
echo "Error: he-docs-lint missing/invalid config: Missing config '$config_path'. Fix: create it (bootstrap should do this) or set HARNESS_DOCS_CONFIG." >&2
|
|
26
|
-
return 1
|
|
27
|
-
fi
|
|
28
|
-
# Validate it is a JSON object
|
|
29
|
-
if ! jq -e 'type == "object"' "$path" >/dev/null 2>&1; then
|
|
30
|
-
echo "Error: he-docs-lint missing/invalid config: Config must be a JSON object." >&2
|
|
31
|
-
return 1
|
|
32
|
-
fi
|
|
33
|
-
cat "$path"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
_gh_annotate() {
|
|
37
|
-
local level="$1" file="$2" title="$3" msg="$4"
|
|
38
|
-
if [[ -n "$file" ]]; then
|
|
39
|
-
echo "::${level} file=${file},title=${title}::${msg}"
|
|
40
|
-
else
|
|
41
|
-
echo "::${level} title=${title}::${msg}"
|
|
42
|
-
fi
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
_emit() {
|
|
46
|
-
local level="$1" file="$2" title="$3" msg="$4"
|
|
47
|
-
_gh_annotate "$level" "$file" "$title" "$msg"
|
|
48
|
-
local upper
|
|
49
|
-
upper="$(echo "$level" | tr '[:lower:]' '[:upper:]')"
|
|
50
|
-
echo "${upper}: ${msg}" >&2
|
|
51
|
-
if [[ "$level" == "error" ]]; then
|
|
52
|
-
ERRORS=$((ERRORS + 1))
|
|
53
|
-
else
|
|
54
|
-
WARNINGS=$((WARNINGS + 1))
|
|
55
|
-
fi
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
_has_exact_line() {
|
|
59
|
-
local path="$1" needle="$2"
|
|
60
|
-
grep -Fxq "$needle" "$path" 2>/dev/null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
# ── Checks ───────────────────────────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
_check_required_docs() {
|
|
66
|
-
local cfg="$1"
|
|
67
|
-
local count
|
|
68
|
-
count="$(echo "$cfg" | jq -r '.required_docs | if type == "array" then length else 0 end')"
|
|
69
|
-
if [[ "$count" -eq 0 ]]; then
|
|
70
|
-
return
|
|
71
|
-
fi
|
|
72
|
-
local i doc
|
|
73
|
-
for ((i = 0; i < count; i++)); do
|
|
74
|
-
doc="$(echo "$cfg" | jq -r ".required_docs[$i]")"
|
|
75
|
-
if [[ "$doc" == "null" ]] || [[ -z "$doc" ]]; then
|
|
76
|
-
continue
|
|
77
|
-
fi
|
|
78
|
-
if [[ ! -e "$REPO_ROOT/$doc" ]]; then
|
|
79
|
-
_emit "error" "$doc" "Required doc missing" \
|
|
80
|
-
"Missing required doc: '$doc'. Fix: create it (run he-bootstrap if this repo is not bootstrapped) or adjust required_docs in config."
|
|
81
|
-
fi
|
|
82
|
-
done
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
_check_domain_doc_headings() {
|
|
86
|
-
local cfg="$1"
|
|
87
|
-
local is_obj
|
|
88
|
-
is_obj="$(echo "$cfg" | jq -r '.required_headings | type')"
|
|
89
|
-
if [[ "$is_obj" != "object" ]]; then
|
|
90
|
-
return
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
local docs
|
|
94
|
-
docs="$(echo "$cfg" | jq -r '.required_headings | keys[]')"
|
|
95
|
-
if [[ -z "$docs" ]]; then
|
|
96
|
-
return
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
local doc
|
|
100
|
-
while IFS= read -r doc; do
|
|
101
|
-
[[ -z "$doc" ]] && continue
|
|
102
|
-
local path="$REPO_ROOT/$doc"
|
|
103
|
-
if [[ ! -f "$path" ]]; then
|
|
104
|
-
continue # on-demand domain docs
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
local headings_count
|
|
108
|
-
headings_count="$(echo "$cfg" | jq -r --arg d "$doc" '.required_headings[$d] | if type == "array" then length else 0 end')"
|
|
109
|
-
if [[ "$headings_count" -eq 0 ]]; then
|
|
110
|
-
_emit "error" "$doc" "Missing config headings" \
|
|
111
|
-
"No required headings configured for '$doc'. Fix: add required_headings['$doc'] in config or remove the entry."
|
|
112
|
-
continue
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
local missing=()
|
|
116
|
-
local j heading
|
|
117
|
-
for ((j = 0; j < headings_count; j++)); do
|
|
118
|
-
heading="$(echo "$cfg" | jq -r --arg d "$doc" ".required_headings[\$d][$j]")"
|
|
119
|
-
if [[ "$heading" == "null" ]] || [[ -z "$heading" ]]; then
|
|
120
|
-
continue
|
|
121
|
-
fi
|
|
122
|
-
if ! _has_exact_line "$path" "$heading"; then
|
|
123
|
-
missing+=("$heading")
|
|
124
|
-
fi
|
|
125
|
-
done
|
|
126
|
-
|
|
127
|
-
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
128
|
-
local joined
|
|
129
|
-
joined="$(printf "%s; " "${missing[@]}")"
|
|
130
|
-
joined="${joined%; }" # trim trailing "; "
|
|
131
|
-
_emit "error" "$doc" "Missing headings" \
|
|
132
|
-
"Missing required headings in '$doc': ${joined}. Fix: add them."
|
|
133
|
-
fi
|
|
134
|
-
done <<< "$docs"
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
_check_seed_markers() {
|
|
138
|
-
local cfg="$1"
|
|
139
|
-
local fail_level="warning"
|
|
140
|
-
if _env_flag "HARNESS_FAIL_ON_SEED_MARKERS" "0"; then
|
|
141
|
-
fail_level="error"
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
local count
|
|
145
|
-
count="$(echo "$cfg" | jq -r '.domain_docs | if type == "array" then length else 0 end')"
|
|
146
|
-
if [[ "$count" -eq 0 ]]; then
|
|
147
|
-
return
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
local i doc path
|
|
151
|
-
for ((i = 0; i < count; i++)); do
|
|
152
|
-
doc="$(echo "$cfg" | jq -r ".domain_docs[$i]")"
|
|
153
|
-
if [[ "$doc" == "null" ]] || [[ -z "$doc" ]]; then
|
|
154
|
-
continue
|
|
155
|
-
fi
|
|
156
|
-
path="$REPO_ROOT/$doc"
|
|
157
|
-
if [[ ! -f "$path" ]]; then
|
|
158
|
-
continue
|
|
159
|
-
fi
|
|
160
|
-
if grep -q '<!-- seed:' "$path" 2>/dev/null; then
|
|
161
|
-
_emit "$fail_level" "$doc" "Seed markers present" \
|
|
162
|
-
"Template seed markers remain in '$doc'. Fix: replace/remove <!-- seed: ... --> blocks once this repo has real domain context."
|
|
163
|
-
fi
|
|
164
|
-
done
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_check_generated_last_updated() {
|
|
168
|
-
local gen_dir="$REPO_ROOT/docs/generated"
|
|
169
|
-
if [[ ! -d "$gen_dir" ]]; then
|
|
170
|
-
return
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
local fail_level="warning"
|
|
174
|
-
if _env_flag "HARNESS_FAIL_ON_GENERATED_PLACEHOLDERS" "0"; then
|
|
175
|
-
fail_level="error"
|
|
176
|
-
fi
|
|
177
|
-
|
|
178
|
-
local path rel
|
|
179
|
-
for path in "$gen_dir"/*.md; do
|
|
180
|
-
[[ -e "$path" ]] || continue # handle no-match glob
|
|
181
|
-
rel="${path#"$REPO_ROOT/"}"
|
|
182
|
-
# Skip known non-generated docs
|
|
183
|
-
if [[ "$rel" == "docs/generated/README.md" ]] || [[ "$rel" == "docs/generated/memory.md" ]]; then
|
|
184
|
-
continue
|
|
185
|
-
fi
|
|
186
|
-
local text
|
|
187
|
-
text="$(cat "$path")"
|
|
188
|
-
|
|
189
|
-
# Check for missing last_updated line (BSD/GNU portable; avoid grep -P)
|
|
190
|
-
if ! echo "$text" | grep -Eq '^[[:space:]]*-[[:space:]]*last_updated:[[:space:]]*'; then
|
|
191
|
-
_emit "error" "$rel" "Missing last_updated" \
|
|
192
|
-
"Generated doc '$rel' must include a 'last_updated' line. Fix: add e.g. '- last_updated: 2026-02-15 12:34'."
|
|
193
|
-
fi
|
|
194
|
-
|
|
195
|
-
# Check for placeholder last_updated value
|
|
196
|
-
if echo "$text" | grep -Eq 'last_updated:[[:space:]]*<YYYY-'; then
|
|
197
|
-
_emit "$fail_level" "$rel" "Placeholder last_updated" \
|
|
198
|
-
"Generated doc '$rel' has a placeholder last_updated value. Fix: replace with a real timestamp."
|
|
199
|
-
fi
|
|
200
|
-
done
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
# ── Main ─────────────────────────────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
main() {
|
|
206
|
-
local cfg
|
|
207
|
-
if ! cfg="$(_load_config)"; then
|
|
208
|
-
exit 2
|
|
209
|
-
fi
|
|
210
|
-
|
|
211
|
-
echo "he-docs-lint: starting"
|
|
212
|
-
echo "Repro: bash scripts/ci/he-docs-lint.sh"
|
|
213
|
-
|
|
214
|
-
_check_required_docs "$cfg"
|
|
215
|
-
|
|
216
|
-
# Runbooks lint is its own script. Fail fast if it fails.
|
|
217
|
-
if ! bash "$REPO_ROOT/scripts/ci/he-runbooks-lint.sh"; then
|
|
218
|
-
exit 1
|
|
219
|
-
fi
|
|
220
|
-
|
|
221
|
-
_check_domain_doc_headings "$cfg"
|
|
222
|
-
_check_seed_markers "$cfg"
|
|
223
|
-
_check_generated_last_updated
|
|
224
|
-
|
|
225
|
-
if [[ "$ERRORS" -gt 0 ]]; then
|
|
226
|
-
echo "he-docs-lint: FAIL ($ERRORS error(s), $WARNINGS warning(s))" >&2
|
|
227
|
-
exit 1
|
|
228
|
-
fi
|
|
229
|
-
|
|
230
|
-
echo "he-docs-lint: OK ($WARNINGS warning(s))"
|
|
231
|
-
exit 0
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
main "$@"
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
# ── Repo root (two levels above this script) ──────────────────────────
|
|
5
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
7
|
-
|
|
8
|
-
DEFAULT_CONFIG_PATH="scripts/ci/he-docs-config.json"
|
|
9
|
-
|
|
10
|
-
# ── Default required headings ─────────────────────────────────────────
|
|
11
|
-
DEFAULT_REQUIRED_HEADINGS=(
|
|
12
|
-
"## Purpose / Big Picture"
|
|
13
|
-
"## Progress"
|
|
14
|
-
"## Surprises & Discoveries"
|
|
15
|
-
"## Decision Log"
|
|
16
|
-
"## Outcomes & Retrospective"
|
|
17
|
-
"## Context and Orientation"
|
|
18
|
-
"## Milestones"
|
|
19
|
-
"## Plan of Work"
|
|
20
|
-
"## Concrete Steps"
|
|
21
|
-
"## Validation and Acceptance"
|
|
22
|
-
"## Idempotence and Recovery"
|
|
23
|
-
"## Artifacts and Notes"
|
|
24
|
-
"## Interfaces and Dependencies"
|
|
25
|
-
"## Pull Request"
|
|
26
|
-
"## Review Findings"
|
|
27
|
-
"## Verify/Release Decision"
|
|
28
|
-
"## Revision Notes"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
# ── Counters ──────────────────────────────────────────────────────────
|
|
32
|
-
ERRORS=0
|
|
33
|
-
WARNINGS=0
|
|
34
|
-
|
|
35
|
-
# ── Emit a finding (GitHub annotation + stderr) ──────────────────────
|
|
36
|
-
emit() {
|
|
37
|
-
local level="$1" file="$2" title="$3" msg="$4"
|
|
38
|
-
if [[ -n "$file" ]]; then
|
|
39
|
-
echo "::${level} file=${file},title=${title}::${msg}"
|
|
40
|
-
else
|
|
41
|
-
echo "::${level} title=${title}::${msg}"
|
|
42
|
-
fi
|
|
43
|
-
echo "${level^^}: ${msg}" >&2
|
|
44
|
-
if [[ "$level" == "error" ]]; then
|
|
45
|
-
(( ERRORS++ )) || true
|
|
46
|
-
else
|
|
47
|
-
(( WARNINGS++ )) || true
|
|
48
|
-
fi
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
# ── Load config ───────────────────────────────────────────────────────
|
|
52
|
-
load_config() {
|
|
53
|
-
local config_rel="${HARNESS_DOCS_CONFIG:-$DEFAULT_CONFIG_PATH}"
|
|
54
|
-
local config_path="$REPO_ROOT/$config_rel"
|
|
55
|
-
if [[ ! -f "$config_path" ]]; then
|
|
56
|
-
echo "Error: he-plans-lint missing/invalid config: Missing config '${config_rel}'. Fix: create it (bootstrap should do this) or set HARNESS_DOCS_CONFIG." >&2
|
|
57
|
-
exit 2
|
|
58
|
-
fi
|
|
59
|
-
# Validate it is a JSON object
|
|
60
|
-
if ! jq -e 'type == "object"' "$config_path" >/dev/null 2>&1; then
|
|
61
|
-
echo "Error: he-plans-lint missing/invalid config: Config must be a JSON object." >&2
|
|
62
|
-
exit 2
|
|
63
|
-
fi
|
|
64
|
-
CONFIG_PATH="$config_path"
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# ── Config helpers ────────────────────────────────────────────────────
|
|
68
|
-
cfg_get() {
|
|
69
|
-
# $1 = jq expression, returns raw output
|
|
70
|
-
jq -r "$1" "$CONFIG_PATH"
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
cfg_get_array() {
|
|
74
|
-
# $1 = jq path to array, outputs one element per line
|
|
75
|
-
jq -r "$1 // [] | if type == \"array\" then .[] else empty end" "$CONFIG_PATH" 2>/dev/null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
# ── Extract YAML frontmatter (between first --- and second ---) ──────
|
|
79
|
-
# Sets FRONTMATTER variable. Returns 1 if no frontmatter found.
|
|
80
|
-
extract_frontmatter() {
|
|
81
|
-
local file="$1"
|
|
82
|
-
FRONTMATTER=""
|
|
83
|
-
local first_line
|
|
84
|
-
first_line="$(head -1 "$file")"
|
|
85
|
-
if [[ "$(echo "$first_line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" != "---" ]]; then
|
|
86
|
-
return 1
|
|
87
|
-
fi
|
|
88
|
-
# Find closing --- (skip line 1, find next ---)
|
|
89
|
-
local end_line
|
|
90
|
-
end_line="$(awk 'NR > 1 && /^[[:space:]]*---[[:space:]]*$/ { print NR; exit }' "$file")"
|
|
91
|
-
if [[ -z "$end_line" ]]; then
|
|
92
|
-
return 1
|
|
93
|
-
fi
|
|
94
|
-
# Extract lines between line 2 and end_line-1
|
|
95
|
-
FRONTMATTER="$(sed -n "2,$((end_line - 1))p" "$file")"
|
|
96
|
-
return 0
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
# ── Parse frontmatter key-value pairs ────────────────────────────────
|
|
100
|
-
# Reads FRONTMATTER, outputs "key=value" lines
|
|
101
|
-
frontmatter_keys=()
|
|
102
|
-
frontmatter_vals=()
|
|
103
|
-
|
|
104
|
-
parse_frontmatter_kv() {
|
|
105
|
-
frontmatter_keys=()
|
|
106
|
-
frontmatter_vals=()
|
|
107
|
-
while IFS= read -r raw; do
|
|
108
|
-
local line
|
|
109
|
-
line="$(echo "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
110
|
-
# Skip empty / comment lines
|
|
111
|
-
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
112
|
-
# Must contain a colon
|
|
113
|
-
[[ "$line" != *:* ]] && continue
|
|
114
|
-
local key val
|
|
115
|
-
key="$(echo "$line" | cut -d: -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
116
|
-
val="$(echo "$line" | cut -d: -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
117
|
-
frontmatter_keys+=("$key")
|
|
118
|
-
frontmatter_vals+=("$val")
|
|
119
|
-
done <<< "$FRONTMATTER"
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# ── Lookup a frontmatter value by key ────────────────────────────────
|
|
123
|
-
fm_get() {
|
|
124
|
-
local needle="$1"
|
|
125
|
-
for i in "${!frontmatter_keys[@]}"; do
|
|
126
|
-
if [[ "${frontmatter_keys[$i]}" == "$needle" ]]; then
|
|
127
|
-
echo "${frontmatter_vals[$i]}"
|
|
128
|
-
return 0
|
|
129
|
-
fi
|
|
130
|
-
done
|
|
131
|
-
return 1
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
fm_has_key() {
|
|
135
|
-
local needle="$1"
|
|
136
|
-
for k in "${frontmatter_keys[@]}"; do
|
|
137
|
-
[[ "$k" == "$needle" ]] && return 0
|
|
138
|
-
done
|
|
139
|
-
return 1
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# ── Extract section lines (between heading and next ## heading) ──────
|
|
143
|
-
# Outputs section body lines to stdout
|
|
144
|
-
section_lines() {
|
|
145
|
-
local file="$1" heading="$2"
|
|
146
|
-
awk -v h="$heading" '
|
|
147
|
-
BEGIN { found=0 }
|
|
148
|
-
$0 == h { found=1; next }
|
|
149
|
-
found && /^## / { exit }
|
|
150
|
-
found { print }
|
|
151
|
-
' "$file"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
# ── Check: exact heading line exists in file ─────────────────────────
|
|
155
|
-
has_exact_line() {
|
|
156
|
-
local file="$1" needle="$2"
|
|
157
|
-
grep -qxF "$needle" "$file"
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
# ── Check: Progress section ──────────────────────────────────────────
|
|
161
|
-
check_progress() {
|
|
162
|
-
local file_rel="$1" file_abs="$2"
|
|
163
|
-
local body
|
|
164
|
-
body="$(section_lines "$file_abs" "## Progress")"
|
|
165
|
-
|
|
166
|
-
# Check non-empty (has at least one non-blank line)
|
|
167
|
-
if ! echo "$body" | grep -q '[^[:space:]]'; then
|
|
168
|
-
emit "error" "$file_rel" "Missing Progress content" \
|
|
169
|
-
"Plan '${file_rel}' has an empty ## Progress section."
|
|
170
|
-
return
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
# Check timestamped checkbox pattern
|
|
174
|
-
if ! echo "$body" | grep -qE '^- \[[ xX]\] \([0-9]{4}-[0-9]{2}-[0-9]{2}[^)]*\) P[0-9]+'; then
|
|
175
|
-
emit "error" "$file_rel" "Progress format" \
|
|
176
|
-
"Plan '${file_rel}' must include timestamped progress checkboxes with IDs (e.g. '- [ ] (2026-02-15T12:00:00Z) P1 ...')."
|
|
177
|
-
fi
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
# ── Check: checklists only in Progress ───────────────────────────────
|
|
181
|
-
check_checklists_only_in_progress() {
|
|
182
|
-
local file_rel="$1" file_abs="$2"
|
|
183
|
-
local bad=0
|
|
184
|
-
local in_progress=0
|
|
185
|
-
while IFS= read -r line; do
|
|
186
|
-
if [[ "$line" == "## Progress" ]]; then
|
|
187
|
-
in_progress=1
|
|
188
|
-
continue
|
|
189
|
-
fi
|
|
190
|
-
if [[ "$line" == "## "* ]]; then
|
|
191
|
-
in_progress=0
|
|
192
|
-
fi
|
|
193
|
-
if [[ $in_progress -eq 0 ]] && echo "$line" | grep -qE '^- \[[ xX]\]'; then
|
|
194
|
-
bad=1
|
|
195
|
-
break
|
|
196
|
-
fi
|
|
197
|
-
done < "$file_abs"
|
|
198
|
-
|
|
199
|
-
if [[ $bad -eq 1 ]]; then
|
|
200
|
-
emit "error" "$file_rel" "Checklist scope" \
|
|
201
|
-
"Plan '${file_rel}' contains checklist items outside ## Progress."
|
|
202
|
-
fi
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# ── Check: Decision Log ──────────────────────────────────────────────
|
|
206
|
-
check_decision_log() {
|
|
207
|
-
local file_rel="$1" file_abs="$2"
|
|
208
|
-
local body
|
|
209
|
-
body="$(section_lines "$file_abs" "## Decision Log")"
|
|
210
|
-
|
|
211
|
-
if ! echo "$body" | grep -q '[^[:space:]]'; then
|
|
212
|
-
emit "error" "$file_rel" "Missing Decision Log content" \
|
|
213
|
-
"Plan '${file_rel}' has an empty ## Decision Log section."
|
|
214
|
-
return
|
|
215
|
-
fi
|
|
216
|
-
|
|
217
|
-
if ! echo "$body" | grep -q '^- Decision:'; then
|
|
218
|
-
emit "error" "$file_rel" "Decision format" \
|
|
219
|
-
"Plan '${file_rel}' should record decisions using '- Decision:' entries."
|
|
220
|
-
fi
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
# ── Check: Revision Notes ────────────────────────────────────────────
|
|
224
|
-
check_revision_notes() {
|
|
225
|
-
local file_rel="$1" file_abs="$2"
|
|
226
|
-
local body
|
|
227
|
-
body="$(section_lines "$file_abs" "## Revision Notes")"
|
|
228
|
-
|
|
229
|
-
if ! echo "$body" | grep -q '[^[:space:]]'; then
|
|
230
|
-
emit "error" "$file_rel" "Missing Revision Notes content" \
|
|
231
|
-
"Plan '${file_rel}' has an empty ## Revision Notes section."
|
|
232
|
-
return
|
|
233
|
-
fi
|
|
234
|
-
|
|
235
|
-
if ! echo "$body" | grep -q '^- '; then
|
|
236
|
-
emit "error" "$file_rel" "Revision Notes format" \
|
|
237
|
-
"Plan '${file_rel}' should include at least one bullet in ## Revision Notes."
|
|
238
|
-
fi
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
# ── Check: placeholder tokens ────────────────────────────────────────
|
|
242
|
-
check_placeholders() {
|
|
243
|
-
local file_rel="$1" file_abs="$2"
|
|
244
|
-
local fail_ph=0
|
|
245
|
-
[[ "${HARNESS_FAIL_ON_ARTIFACT_PLACEHOLDERS:-0}" == "1" ]] && fail_ph=1
|
|
246
|
-
|
|
247
|
-
local text
|
|
248
|
-
text="$(cat "$file_abs")"
|
|
249
|
-
|
|
250
|
-
while IFS= read -r pattern; do
|
|
251
|
-
[[ -z "$pattern" ]] && continue
|
|
252
|
-
if echo "$text" | grep -qF "$pattern"; then
|
|
253
|
-
local level="warning"
|
|
254
|
-
local msg="Plan '${file_rel}' contains placeholder token '${pattern}'."
|
|
255
|
-
if [[ $fail_ph -eq 1 ]]; then
|
|
256
|
-
level="error"
|
|
257
|
-
else
|
|
258
|
-
msg="${msg} (Set HARNESS_FAIL_ON_ARTIFACT_PLACEHOLDERS=1 to enforce.)"
|
|
259
|
-
fi
|
|
260
|
-
emit "$level" "$file_rel" "Placeholder token" "$msg"
|
|
261
|
-
break
|
|
262
|
-
fi
|
|
263
|
-
done < <(cfg_get_array '.artifact_placeholder_patterns')
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
# ── Check a single plan file ─────────────────────────────────────────
|
|
267
|
-
check_plan() {
|
|
268
|
-
local file_abs="$1"
|
|
269
|
-
local file_rel="${file_abs#"$REPO_ROOT/"}"
|
|
270
|
-
|
|
271
|
-
# Frontmatter
|
|
272
|
-
if ! extract_frontmatter "$file_abs"; then
|
|
273
|
-
emit "error" "$file_rel" "Missing YAML frontmatter" \
|
|
274
|
-
"Plan '${file_rel}' must start with YAML frontmatter delimited by '---' lines."
|
|
275
|
-
return
|
|
276
|
-
fi
|
|
277
|
-
|
|
278
|
-
parse_frontmatter_kv
|
|
279
|
-
|
|
280
|
-
# Required frontmatter keys
|
|
281
|
-
while IFS= read -r key; do
|
|
282
|
-
[[ -z "$key" ]] && continue
|
|
283
|
-
if ! fm_has_key "$key"; then
|
|
284
|
-
emit "error" "$file_rel" "Missing frontmatter key" \
|
|
285
|
-
"Plan '${file_rel}' missing YAML frontmatter key '${key}:'."
|
|
286
|
-
fi
|
|
287
|
-
done < <(cfg_get_array '.required_plan_frontmatter_keys')
|
|
288
|
-
|
|
289
|
-
# plan_mode validation
|
|
290
|
-
local plan_mode
|
|
291
|
-
plan_mode="$(fm_get "plan_mode" 2>/dev/null || true)"
|
|
292
|
-
plan_mode="$(echo "$plan_mode" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
293
|
-
if [[ -n "$plan_mode" && "$plan_mode" != "trivial" && "$plan_mode" != "lightweight" && "$plan_mode" != "execution" ]]; then
|
|
294
|
-
emit "error" "$file_rel" "Invalid plan_mode" \
|
|
295
|
-
"Plan '${file_rel}' has invalid plan_mode '${plan_mode}' (must be 'trivial', 'lightweight', or 'execution')."
|
|
296
|
-
fi
|
|
297
|
-
|
|
298
|
-
# Required headings
|
|
299
|
-
for h in "${DEFAULT_REQUIRED_HEADINGS[@]}"; do
|
|
300
|
-
if ! has_exact_line "$file_abs" "$h"; then
|
|
301
|
-
emit "error" "$file_rel" "Missing heading" \
|
|
302
|
-
"Plan '${file_rel}' missing required heading line '${h}'."
|
|
303
|
-
fi
|
|
304
|
-
done
|
|
305
|
-
|
|
306
|
-
# Section-level checks
|
|
307
|
-
check_progress "$file_rel" "$file_abs"
|
|
308
|
-
check_checklists_only_in_progress "$file_rel" "$file_abs"
|
|
309
|
-
check_decision_log "$file_rel" "$file_abs"
|
|
310
|
-
check_revision_notes "$file_rel" "$file_abs"
|
|
311
|
-
check_placeholders "$file_rel" "$file_abs"
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
315
|
-
# Main
|
|
316
|
-
# ══════════════════════════════════════════════════════════════════════
|
|
317
|
-
load_config
|
|
318
|
-
|
|
319
|
-
echo "he-plans-lint: starting"
|
|
320
|
-
echo "Repro: bash scripts/ci/he-plans-lint.sh"
|
|
321
|
-
|
|
322
|
-
plans_active="$REPO_ROOT/docs/plans/active"
|
|
323
|
-
plans_completed="$REPO_ROOT/docs/plans/completed"
|
|
324
|
-
|
|
325
|
-
files=()
|
|
326
|
-
if [[ -d "$plans_active" ]]; then
|
|
327
|
-
while IFS= read -r -d '' f; do
|
|
328
|
-
files+=("$f")
|
|
329
|
-
done < <(find "$plans_active" -maxdepth 1 -name '*.md' -print0 | sort -z)
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
lint_completed="$(cfg_get '.lint_completed_plans // true')"
|
|
333
|
-
if [[ "$lint_completed" != "false" && -d "$plans_completed" ]]; then
|
|
334
|
-
while IFS= read -r -d '' f; do
|
|
335
|
-
files+=("$f")
|
|
336
|
-
done < <(find "$plans_completed" -maxdepth 1 -name '*.md' -print0 | sort -z)
|
|
337
|
-
fi
|
|
338
|
-
|
|
339
|
-
if [[ ${#files[@]} -eq 0 ]]; then
|
|
340
|
-
echo "he-plans-lint: OK (no plan files)"
|
|
341
|
-
exit 0
|
|
342
|
-
fi
|
|
343
|
-
|
|
344
|
-
for f in "${files[@]}"; do
|
|
345
|
-
check_plan "$f"
|
|
346
|
-
done
|
|
347
|
-
|
|
348
|
-
if [[ $ERRORS -gt 0 ]]; then
|
|
349
|
-
echo "he-plans-lint: FAIL (${ERRORS} error(s), ${WARNINGS} warning(s))" >&2
|
|
350
|
-
exit 1
|
|
351
|
-
fi
|
|
352
|
-
|
|
353
|
-
echo "he-plans-lint: OK (${WARNINGS} warning(s))"
|
|
354
|
-
exit 0
|