@house365/comate 0.10.2 → 1.0.2-next.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/LICENSE +21 -21
- package/README.md +104 -64
- package/bin/cli.mjs +197 -190
- package/bin/doctor.mjs +29 -41
- package/bin/init-run.mjs +310 -368
- package/bin/init.mjs +53 -71
- package/bin/postinstall.mjs +56 -56
- package/docs/CHANGELOG-fix-simplification.md +145 -0
- package/docs/COMATE_BETA.md +90 -0
- package/docs/COMATE_MANUAL.md +239 -665
- package/docs/superpowers/plans/2026-10-14-review-fixes.md +48 -0
- package/docs/superpowers/plans/2026-10-14-workflow-quality.md +48 -0
- package/package.json +54 -55
- package/skills/comate/SKILL.md +42 -325
- package/skills/comate/reference/archive.md +50 -0
- package/skills/comate/reference/build.md +67 -0
- package/skills/comate/reference/fix.md +150 -0
- package/skills/comate/reference/open.md +162 -0
- package/skills/comate/reference/verify.md +123 -0
- package/skills/comate/scaffold/comate/README.md +18 -0
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/architecture/README.md +5 -6
- package/skills/comate/scaffold/comate/comate/config.json +10 -0
- package/skills/comate/scaffold/comate/knowledge-base/README.md +42 -0
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/_TEMPLATE/README.md +2 -2
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/_TEMPLATE/api-snapshot.md +47 -47
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/requirements/_templates/PRD.md +4 -4
- package/skills/comate/scripts/comate.mjs +147 -0
- package/skills/comate/scripts/lib/acceptance.mjs +112 -0
- package/skills/comate/scripts/lib/brief.mjs +50 -0
- package/skills/comate/scripts/lib/checks.mjs +178 -0
- package/skills/comate/scripts/lib/cmd-archive.mjs +152 -0
- package/skills/comate/scripts/lib/cmd-decide.mjs +116 -0
- package/skills/comate/scripts/lib/cmd-init.mjs +75 -0
- package/skills/comate/scripts/lib/cmd-loop.mjs +273 -0
- package/skills/comate/scripts/lib/cmd-open.mjs +161 -0
- package/skills/comate/scripts/lib/cmd-status.mjs +121 -0
- package/skills/comate/scripts/lib/common.mjs +85 -0
- package/skills/comate/scripts/lib/docs.mjs +45 -0
- package/skills/comate/scripts/lib/finish.mjs +108 -0
- package/skills/comate/scripts/lib/git.mjs +112 -0
- package/skills/comate/scripts/lib/md.mjs +85 -0
- package/skills/comate/scripts/lib/next.mjs +217 -0
- package/skills/comate/scripts/lib/paths.mjs +96 -0
- package/skills/comate/scripts/lib/render.mjs +46 -0
- package/skills/comate/scripts/lib/spec.mjs +106 -0
- package/skills/comate/scripts/lib/state.mjs +128 -0
- package/skills/comate/scripts/lib/tasks.mjs +53 -0
- package/skills/comate/scripts/lib/templates.mjs +58 -0
- package/skills/comate/scripts/lib/util.mjs +108 -0
- package/skills/comate-kb-integration/SKILL.md +128 -128
- package/skills/comate-kb-integration/scripts/fetch_rendered_doc.py +84 -84
- package/bin/cleanup-duplicate-superpowers.mjs +0 -119
- package/docs/CLEANUP_SUPERPOWERS.md +0 -128
- package/docs/COMATE_BEGINNER_GUIDE.md +0 -188
- package/docs/COMATE_INIT_DIRECTORY_GUIDE.md +0 -201
- package/docs/SUPERPOWERS_FIX_SUMMARY.md +0 -114
- package/skills/comate/reference/decision-points.md +0 -103
- package/skills/comate/reference/dirty-worktree.md +0 -58
- package/skills/comate/reference/prerequisites.md +0 -89
- package/skills/comate/scripts/comate-lib.sh +0 -559
- package/skills/comate/scripts/comate.sh +0 -2147
- package/skills/comate/tests/comate.bats +0 -1019
- package/skills/comate/tests/run.sh +0 -101
- package/skills/comate-archive/SKILL.md +0 -63
- package/skills/comate-build/SKILL.md +0 -278
- package/skills/comate-design/SKILL.md +0 -178
- package/skills/comate-fix/SKILL.md +0 -195
- package/skills/comate-gate/SKILL.md +0 -316
- package/skills/comate-gate/scaffold/docs/README.md +0 -36
- package/skills/comate-gate/scaffold/docs/knowledge-base/README.md +0 -47
- package/skills/comate-grill/SKILL.md +0 -185
- package/skills/comate-open/SKILL.md +0 -230
- package/skills/comate-verify/SKILL.md +0 -212
- /package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/README.md +0 -0
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# comate-lib.sh — shared library for comate (sourced, never executed directly)
|
|
3
|
-
#
|
|
4
|
-
# Single source of truth for: YAML flat read/write, field-order template + enums,
|
|
5
|
-
# hashing, logging, exit-code contract. Other scripts `source` this file.
|
|
6
|
-
#
|
|
7
|
-
# - all YAML logic lives here
|
|
8
|
-
# - .comate.yaml is a declared flat `key: value` subset; validate_flat_yaml enforces it.
|
|
9
|
-
# - yaml_set does full-rewrite + atomic mv + write-back verification.
|
|
10
|
-
# - stable exit codes — 0 OK | 1 BLOCK | 2 USAGE | 3 ENV | 4 CORRUPT.
|
|
11
|
-
#
|
|
12
|
-
# This file MUST NOT set `set -e` / `set -u`: it is sourced into hosts that
|
|
13
|
-
# manage their own shell options.
|
|
14
|
-
|
|
15
|
-
# --- Exit-code contract ---
|
|
16
|
-
readonly COMATE_OK=0
|
|
17
|
-
readonly COMATE_BLOCK=1
|
|
18
|
-
readonly COMATE_USAGE=2
|
|
19
|
-
readonly COMATE_ENV=3
|
|
20
|
-
readonly COMATE_CORRUPT=4
|
|
21
|
-
|
|
22
|
-
# --- Color logging on stderr ---
|
|
23
|
-
_comate_color() {
|
|
24
|
-
# $1 = ansi code, $2 = message
|
|
25
|
-
if [ -t 2 ]; then
|
|
26
|
-
printf '\033[%sm%s\033[0m\n' "$1" "$2" >&2
|
|
27
|
-
else
|
|
28
|
-
printf '%s\n' "$2" >&2
|
|
29
|
-
fi
|
|
30
|
-
}
|
|
31
|
-
log_pass() { _comate_color "32" "$1"; }
|
|
32
|
-
log_fail() { _comate_color "31" "$1"; }
|
|
33
|
-
log_warn() { _comate_color "33" "$1"; }
|
|
34
|
-
log_info() { _comate_color "36" "$1"; }
|
|
35
|
-
|
|
36
|
-
# die <code> <msg> — print red error to stderr and exit with the given code.
|
|
37
|
-
die() {
|
|
38
|
-
local code="$1"
|
|
39
|
-
shift
|
|
40
|
-
_comate_color "31" "$*"
|
|
41
|
-
exit "$code"
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
# --- Canonical field order ---
|
|
45
|
-
# Every yaml_set rewrites the whole file in this order, so diffs stay stable
|
|
46
|
-
# and duplicate keys are impossible.
|
|
47
|
-
# Three-place sync rule: a new field must be added here, in ENUMS (if enum),
|
|
48
|
-
# and in the bats yaml fixtures.
|
|
49
|
-
FIELD_ORDER=(
|
|
50
|
-
schema_version
|
|
51
|
-
workflow
|
|
52
|
-
phase
|
|
53
|
-
phase_step
|
|
54
|
-
phase_step_data
|
|
55
|
-
build_mode
|
|
56
|
-
tdd_mode
|
|
57
|
-
isolation
|
|
58
|
-
verify_mode
|
|
59
|
-
verify_result
|
|
60
|
-
design_doc
|
|
61
|
-
plan
|
|
62
|
-
verification_report
|
|
63
|
-
branch_status
|
|
64
|
-
handoff_context
|
|
65
|
-
handoff_hash
|
|
66
|
-
impl_context
|
|
67
|
-
impl_hash
|
|
68
|
-
direct_override
|
|
69
|
-
base_ref
|
|
70
|
-
base_branch
|
|
71
|
-
source_kind
|
|
72
|
-
source_manifest
|
|
73
|
-
build_command
|
|
74
|
-
verify_command
|
|
75
|
-
created_at
|
|
76
|
-
verified_at
|
|
77
|
-
archived
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
# Current schema version written by `state init`.
|
|
81
|
-
readonly COMATE_SCHEMA_VERSION=1
|
|
82
|
-
|
|
83
|
-
# --- Enum tables ---
|
|
84
|
-
# Keep these as case-based lookups instead of associative arrays. macOS ships
|
|
85
|
-
# Bash 3.2, which has normal arrays but not `declare -A`.
|
|
86
|
-
enum_keys() {
|
|
87
|
-
printf '%s\n' \
|
|
88
|
-
schema_version workflow phase build_mode tdd_mode isolation verify_mode \
|
|
89
|
-
verify_result branch_status direct_override archived source_kind
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
enum_values() {
|
|
93
|
-
case "$1" in
|
|
94
|
-
schema_version) printf '%s\n' '1' ;;
|
|
95
|
-
workflow) printf '%s\n' 'full fix' ;;
|
|
96
|
-
phase) printf '%s\n' 'open design build verify archive' ;;
|
|
97
|
-
build_mode) printf '%s\n' 'subagent-driven-development executing-plans direct' ;;
|
|
98
|
-
tdd_mode) printf '%s\n' 'tdd direct' ;;
|
|
99
|
-
isolation) printf '%s\n' 'branch worktree' ;;
|
|
100
|
-
verify_mode) printf '%s\n' 'light full' ;;
|
|
101
|
-
verify_result) printf '%s\n' 'pending pass fail' ;;
|
|
102
|
-
branch_status) printf '%s\n' 'pending handled discarded' ;;
|
|
103
|
-
direct_override) printf '%s\n' 'true false' ;;
|
|
104
|
-
archived) printf '%s\n' 'true false' ;;
|
|
105
|
-
source_kind) printf '%s\n' 'conversation file' ;;
|
|
106
|
-
*) return 1 ;;
|
|
107
|
-
esac
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
# Keys whose values are free-form (paths, dates, commands, hashes, refs).
|
|
111
|
-
# Listed as a case lookup so unknown fields can still be rejected on Bash 3.2.
|
|
112
|
-
is_freeform_key() {
|
|
113
|
-
case "$1" in
|
|
114
|
-
design_doc|plan|verification_report|handoff_context|handoff_hash|\
|
|
115
|
-
impl_context|impl_hash|base_ref|base_branch|source_manifest|\
|
|
116
|
-
build_command|verify_command|created_at|verified_at|phase_step|phase_step_data)
|
|
117
|
-
return 0 ;;
|
|
118
|
-
*) return 1 ;;
|
|
119
|
-
esac
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# --- Path location ---
|
|
123
|
-
|
|
124
|
-
# comate_change_dir <name> — echo the change directory, preferring the active
|
|
125
|
-
# location but falling back to archive/ if that is where it lives.
|
|
126
|
-
comate_change_dir() {
|
|
127
|
-
local name="$1"
|
|
128
|
-
if [ -d "openspec/changes/$name" ]; then
|
|
129
|
-
echo "openspec/changes/$name"
|
|
130
|
-
elif [ -d "openspec/changes/archive/$name" ]; then
|
|
131
|
-
echo "openspec/changes/archive/$name"
|
|
132
|
-
else
|
|
133
|
-
echo "openspec/changes/$name"
|
|
134
|
-
fi
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
# comate_yaml_for <name> — echo path to that change's .comate.yaml.
|
|
138
|
-
comate_yaml_for() {
|
|
139
|
-
echo "$(comate_change_dir "$1")/.comate.yaml"
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# --- Input validation ---
|
|
143
|
-
|
|
144
|
-
# validate_change_name <name> — only [A-Za-z0-9_-]; reject empty and `..`.
|
|
145
|
-
validate_change_name() {
|
|
146
|
-
local name="$1"
|
|
147
|
-
if [ -z "$name" ]; then
|
|
148
|
-
die "$COMATE_USAGE" "ERROR: change name cannot be empty"
|
|
149
|
-
fi
|
|
150
|
-
if [[ "$name" == *..* ]]; then
|
|
151
|
-
die "$COMATE_USAGE" "ERROR: change name cannot contain '..' (path traversal not allowed): '$name'"
|
|
152
|
-
fi
|
|
153
|
-
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
154
|
-
die "$COMATE_USAGE" "ERROR: invalid change name '$name' (allowed: a-z A-Z 0-9 - _)"
|
|
155
|
-
fi
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
# validate_enum <key> <value> — die if value not legal for an enum key.
|
|
159
|
-
# Free-form keys and unlisted keys pass through. null/empty always allowed.
|
|
160
|
-
validate_enum() {
|
|
161
|
-
local key="$1" value="$2"
|
|
162
|
-
local legal
|
|
163
|
-
legal=$(enum_values "$key" 2>/dev/null) || legal=""
|
|
164
|
-
[ -z "$legal" ] && return 0
|
|
165
|
-
[ -z "$value" ] && return 0
|
|
166
|
-
[ "$value" = "null" ] && return 0
|
|
167
|
-
local v
|
|
168
|
-
for v in $legal; do
|
|
169
|
-
[ "$value" = "$v" ] && return 0
|
|
170
|
-
done
|
|
171
|
-
die "$COMATE_USAGE" "ERROR: invalid value '$value' for '$key' (expected: $legal)"
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
# --- YAML read (single implementation) ---
|
|
175
|
-
|
|
176
|
-
# _strip_inline_comment — drop a ` #...` comment that is not inside quotes.
|
|
177
|
-
_strip_inline_comment() {
|
|
178
|
-
printf '%s\n' "$1" | awk -v squote="'" '
|
|
179
|
-
{
|
|
180
|
-
out = ""; quote = ""
|
|
181
|
-
for (i = 1; i <= length($0); i++) {
|
|
182
|
-
c = substr($0, i, 1)
|
|
183
|
-
if (quote == "") {
|
|
184
|
-
if (c == "\"" || c == squote) { quote = c }
|
|
185
|
-
else if (c == "#" && (i == 1 || substr($0, i - 1, 1) ~ /[[:space:]]/)) {
|
|
186
|
-
sub(/[[:space:]]+$/, "", out); print out; next
|
|
187
|
-
}
|
|
188
|
-
} else if (c == quote) { quote = "" }
|
|
189
|
-
out = out c
|
|
190
|
-
}
|
|
191
|
-
print out
|
|
192
|
-
}'
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
# _strip_wrapping_quotes — remove a single pair of wrapping quotes.
|
|
196
|
-
_strip_wrapping_quotes() {
|
|
197
|
-
local value="$1"
|
|
198
|
-
case "$value" in
|
|
199
|
-
\"*\") printf '%s\n' "${value:1:${#value}-2}" ;;
|
|
200
|
-
\'*\') printf '%s\n' "${value:1:${#value}-2}" ;;
|
|
201
|
-
*) printf '%s\n' "$value" ;;
|
|
202
|
-
esac
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# yaml_get <file> <key> — echo the value (comment/quote-stripped), empty if absent.
|
|
206
|
-
yaml_get() {
|
|
207
|
-
local file="$1" key="$2"
|
|
208
|
-
[ -f "$file" ] || return 0
|
|
209
|
-
local value
|
|
210
|
-
value=$(grep "^${key}:" "$file" 2>/dev/null | head -1 | sed "s/^${key}:[[:space:]]*//" || true)
|
|
211
|
-
value=$(_strip_inline_comment "$value")
|
|
212
|
-
_strip_wrapping_quotes "$value"
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
# yaml_keys <file> — list all top-level keys, one per line.
|
|
216
|
-
yaml_keys() {
|
|
217
|
-
local file="$1"
|
|
218
|
-
[ -f "$file" ] || return 0
|
|
219
|
-
grep -E '^[A-Za-z0-9_-]+:' "$file" 2>/dev/null | sed 's/:.*//' || true
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
# validate_flat_yaml <file> — enforce that .comate.yaml is the
|
|
223
|
-
# declared subset: top-level `key: value` scalars plus `#` comments only.
|
|
224
|
-
# Reject indented lines, list items, block/flow collections. die(CORRUPT) on violation.
|
|
225
|
-
validate_flat_yaml() {
|
|
226
|
-
local file="$1"
|
|
227
|
-
[ -f "$file" ] || die "$COMATE_CORRUPT" "ERROR: state file not found: $file"
|
|
228
|
-
local lineno=0 line
|
|
229
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
230
|
-
lineno=$((lineno + 1))
|
|
231
|
-
# Allow blank lines and full-line comments.
|
|
232
|
-
case "$line" in
|
|
233
|
-
"" ) continue ;;
|
|
234
|
-
esac
|
|
235
|
-
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
|
236
|
-
continue
|
|
237
|
-
fi
|
|
238
|
-
# Reject leading whitespace (nesting / continuation / list under a key).
|
|
239
|
-
if [[ "$line" =~ ^[[:space:]]+ ]]; then
|
|
240
|
-
die "$COMATE_CORRUPT" "ERROR: $file:$lineno indented line not allowed (flat key:value only): $line"
|
|
241
|
-
fi
|
|
242
|
-
# Reject top-level list items.
|
|
243
|
-
if [[ "$line" =~ ^- ]]; then
|
|
244
|
-
die "$COMATE_CORRUPT" "ERROR: $file:$lineno list item not allowed (flat key:value only): $line"
|
|
245
|
-
fi
|
|
246
|
-
# Require a `key: value` shape.
|
|
247
|
-
if [[ ! "$line" =~ ^[A-Za-z0-9_-]+: ]]; then
|
|
248
|
-
die "$COMATE_CORRUPT" "ERROR: $file:$lineno not a 'key: value' line: $line"
|
|
249
|
-
fi
|
|
250
|
-
# Reject values that open a block scalar (| or >).
|
|
251
|
-
local val
|
|
252
|
-
val=$(printf '%s\n' "$line" | sed 's/^[A-Za-z0-9_-]*:[[:space:]]*//')
|
|
253
|
-
val=$(_strip_inline_comment "$val")
|
|
254
|
-
case "$val" in
|
|
255
|
-
"|"|">"|"|-"|">-"|"|+"|">+")
|
|
256
|
-
die "$COMATE_CORRUPT" "ERROR: $file:$lineno block scalar not allowed (flat key:value only): $line" ;;
|
|
257
|
-
esac
|
|
258
|
-
done < "$file"
|
|
259
|
-
return 0
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
# --- YAML write (full rewrite + atomic mv + write-back check) ---
|
|
263
|
-
|
|
264
|
-
# yaml_set <file> <key> <value> — validate enum, read whole file, set key in
|
|
265
|
-
# memory, rewrite every field in FIELD_ORDER to a temp file, atomically mv into
|
|
266
|
-
# place, then read back and confirm the value matches.
|
|
267
|
-
yaml_set() {
|
|
268
|
-
local file="$1" key="$2" value="$3"
|
|
269
|
-
[ -f "$file" ] || die "$COMATE_CORRUPT" "ERROR: state file not found: $file"
|
|
270
|
-
|
|
271
|
-
# Reject keys we do not know about (prevents silent typo fields).
|
|
272
|
-
if ! enum_values "$key" >/dev/null 2>&1 && ! is_freeform_key "$key" && [ "$key" != "schema_version" ]; then
|
|
273
|
-
die "$COMATE_USAGE" "ERROR: unknown field '$key'"
|
|
274
|
-
fi
|
|
275
|
-
validate_enum "$key" "$value"
|
|
276
|
-
|
|
277
|
-
local tmp
|
|
278
|
-
tmp=$(mktemp "${TMPDIR:-/tmp}/comate.XXXXXX") || die "$COMATE_ENV" "ERROR: mktemp failed"
|
|
279
|
-
{
|
|
280
|
-
printf '# comate state — flat key:value only, no nesting\n'
|
|
281
|
-
local k
|
|
282
|
-
for k in "${FIELD_ORDER[@]}"; do
|
|
283
|
-
local current
|
|
284
|
-
if [ "$k" = "$key" ]; then
|
|
285
|
-
current="$value"
|
|
286
|
-
else
|
|
287
|
-
current=$(yaml_get "$file" "$k")
|
|
288
|
-
fi
|
|
289
|
-
[ -z "$current" ] && current="null"
|
|
290
|
-
printf '%s: %s\n' "$k" "$current"
|
|
291
|
-
done
|
|
292
|
-
} > "$tmp"
|
|
293
|
-
|
|
294
|
-
mv "$tmp" "$file" || { rm -f "$tmp"; die "$COMATE_ENV" "ERROR: failed to replace $file"; }
|
|
295
|
-
|
|
296
|
-
# Write-back verification.
|
|
297
|
-
local readback
|
|
298
|
-
readback=$(yaml_get "$file" "$key")
|
|
299
|
-
if [ "$readback" != "$value" ]; then
|
|
300
|
-
die "$COMATE_CORRUPT" "ERROR: write-back mismatch for '$key': wrote '$value', read '$readback'"
|
|
301
|
-
fi
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
# --- Hashing (cross-platform) ---
|
|
305
|
-
|
|
306
|
-
# _hash_stream — sha256 of stdin, hex only.
|
|
307
|
-
_hash_stream() {
|
|
308
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
309
|
-
sha256sum | awk '{print $1}'
|
|
310
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
311
|
-
shasum -a 256 | awk '{print $1}'
|
|
312
|
-
else
|
|
313
|
-
die "$COMATE_ENV" "ERROR: sha256sum or shasum is required"
|
|
314
|
-
fi
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
# _hash_file_stream <file> - emit canonical content for hashing.
|
|
318
|
-
# Text files are normalized to LF so a checkout/tool that temporarily writes
|
|
319
|
-
# CRLF cannot create a false provenance or impl_hash drift. Binary files stay
|
|
320
|
-
# byte-for-byte unchanged.
|
|
321
|
-
_hash_file_stream() {
|
|
322
|
-
local file="$1"
|
|
323
|
-
if [ ! -s "$file" ] || LC_ALL=C grep -Iq . "$file" 2>/dev/null; then
|
|
324
|
-
sed 's/\r$//' "$file"
|
|
325
|
-
else
|
|
326
|
-
cat "$file"
|
|
327
|
-
fi
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
# hash_file <file> - sha256 hex of one file (LF-canonical for text).
|
|
331
|
-
hash_file() {
|
|
332
|
-
local file="$1"
|
|
333
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
334
|
-
_hash_file_stream "$file" | sha256sum | awk '{print $1}'
|
|
335
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
336
|
-
_hash_file_stream "$file" | shasum -a 256 | awk '{print $1}'
|
|
337
|
-
else
|
|
338
|
-
die "$COMATE_ENV" "ERROR: sha256sum or shasum is required"
|
|
339
|
-
fi
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
# hash_files_manifest <file...> — for each existing file emit `path:`/`sha256:`
|
|
343
|
-
# lines, then hash the whole manifest. Order-stable, missing files skipped.
|
|
344
|
-
hash_files_manifest() {
|
|
345
|
-
local f
|
|
346
|
-
for f in "$@"; do
|
|
347
|
-
if [ -f "$f" ]; then
|
|
348
|
-
printf 'path:%s\n' "$f"
|
|
349
|
-
printf 'sha256:%s\n' "$(hash_file "$f")"
|
|
350
|
-
fi
|
|
351
|
-
done | _hash_stream
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
# file_nonempty <path>
|
|
355
|
-
file_nonempty() { [ -f "$1" ] && [ -s "$1" ]; }
|
|
356
|
-
|
|
357
|
-
# --- Source tracking (knowledge-base provenance) ---
|
|
358
|
-
|
|
359
|
-
# comate_kb_root — repo-relative root under which source files may be tracked.
|
|
360
|
-
comate_kb_root() { echo "docs/knowledge-base"; }
|
|
361
|
-
|
|
362
|
-
# _abs_path <path> — absolute, symlink-resolved path; empty + non-zero if it
|
|
363
|
-
# cannot be resolved. realpath/readlink -f preferred; dir-level fallback otherwise.
|
|
364
|
-
_abs_path() {
|
|
365
|
-
local p="$1"
|
|
366
|
-
if command -v realpath >/dev/null 2>&1; then
|
|
367
|
-
realpath "$p" 2>/dev/null
|
|
368
|
-
elif command -v readlink >/dev/null 2>&1 && readlink -f "$p" >/dev/null 2>&1; then
|
|
369
|
-
readlink -f "$p" 2>/dev/null
|
|
370
|
-
else
|
|
371
|
-
local dir base
|
|
372
|
-
dir=$(dirname "$p"); base=$(basename "$p")
|
|
373
|
-
( cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$base" )
|
|
374
|
-
fi
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
# validate_kb_path <path> — echo the repo-relative path if it resolves to an
|
|
378
|
-
# existing file under the knowledge-base root, else echo nothing and return 1.
|
|
379
|
-
# Rejects '..' traversal, absolute/repo-external paths, and symlink escapes.
|
|
380
|
-
validate_kb_path() {
|
|
381
|
-
local raw="$1" root_abs target_abs cwd
|
|
382
|
-
[ -n "$raw" ] || return 1
|
|
383
|
-
case "$raw" in *..*) return 1 ;; esac
|
|
384
|
-
[ -f "$raw" ] || return 1
|
|
385
|
-
root_abs=$(_abs_path "$(comate_kb_root)") || return 1
|
|
386
|
-
[ -n "$root_abs" ] || return 1
|
|
387
|
-
target_abs=$(_abs_path "$raw") || return 1
|
|
388
|
-
[ -n "$target_abs" ] || return 1
|
|
389
|
-
case "$target_abs" in
|
|
390
|
-
"$root_abs"/*) ;;
|
|
391
|
-
*) return 1 ;;
|
|
392
|
-
esac
|
|
393
|
-
cwd=$(_abs_path .) || return 1
|
|
394
|
-
printf '%s\n' "${target_abs#"$cwd"/}"
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
# sources_manifest_for <name> — echo path to a change's source manifest.
|
|
398
|
-
sources_manifest_for() {
|
|
399
|
-
echo "$(comate_change_dir "$1")/.comate/sources.manifest"
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
# sources_manifest_paths <manifest> — list recorded `path:` values, one per line.
|
|
403
|
-
sources_manifest_paths() {
|
|
404
|
-
local m="$1"
|
|
405
|
-
[ -f "$m" ] || return 0
|
|
406
|
-
grep '^path:' "$m" 2>/dev/null | sed 's/^path://' || true
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
# sources_drift <manifest> — recompute sha256 for each recorded path and compare
|
|
410
|
-
# against the manifest. Echo human-readable detail; return COMATE_OK if all
|
|
411
|
-
# match, COMATE_BLOCK on drift/missing file, COMATE_CORRUPT on malformed entries.
|
|
412
|
-
sources_drift() {
|
|
413
|
-
local m="$1" path="" recorded="" rc=0 line actual
|
|
414
|
-
[ -f "$m" ] || { echo "manifest not found: $m"; return "$COMATE_CORRUPT"; }
|
|
415
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
416
|
-
case "$line" in
|
|
417
|
-
path:*) path="${line#path:}" ;;
|
|
418
|
-
sha256:*)
|
|
419
|
-
recorded="${line#sha256:}"
|
|
420
|
-
if [ -z "$path" ]; then echo "manifest corrupt: sha256 with no preceding path"; return "$COMATE_CORRUPT"; fi
|
|
421
|
-
if [ ! -f "$path" ]; then
|
|
422
|
-
echo "source file missing: $path"; rc=1
|
|
423
|
-
else
|
|
424
|
-
actual=$(hash_file "$path")
|
|
425
|
-
if [ "$actual" != "$recorded" ]; then
|
|
426
|
-
echo "source drifted: $path"
|
|
427
|
-
echo " recorded: $recorded"
|
|
428
|
-
echo " actual: $actual"
|
|
429
|
-
rc=1
|
|
430
|
-
fi
|
|
431
|
-
fi
|
|
432
|
-
;;
|
|
433
|
-
---|"") path=""; recorded="" ;;
|
|
434
|
-
esac
|
|
435
|
-
done < "$m"
|
|
436
|
-
[ "$rc" -eq 0 ] && return "$COMATE_OK"
|
|
437
|
-
return "$COMATE_BLOCK"
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
# json_escape <string> — escape backslash and double-quote for JSON values.
|
|
441
|
-
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
|
442
|
-
|
|
443
|
-
# --- PRD gate (phase 0: quality gate + split) ---
|
|
444
|
-
#
|
|
445
|
-
# gate.yaml lives NEXT TO the PRD (<prd>.gate.yaml), not under openspec/changes,
|
|
446
|
-
# so it has its own flat schema kept separate from the change .comate.yaml
|
|
447
|
-
# FIELD_ORDER (mixing them would pollute every change state file with gate keys).
|
|
448
|
-
# It is the machine truth for "may a change be spawned from this PRD"; the
|
|
449
|
-
# markdown review report is the human-readable derivative.
|
|
450
|
-
|
|
451
|
-
readonly GATE_SCHEMA_VERSION=1
|
|
452
|
-
|
|
453
|
-
# Canonical field order for gate.yaml (same full-rewrite discipline as yaml_set).
|
|
454
|
-
GATE_FIELD_ORDER=(
|
|
455
|
-
schema_version
|
|
456
|
-
prd_path
|
|
457
|
-
prd_hash
|
|
458
|
-
gate_status
|
|
459
|
-
score
|
|
460
|
-
blocking_open
|
|
461
|
-
review_report
|
|
462
|
-
spawned_changes
|
|
463
|
-
changes_map
|
|
464
|
-
gated_at
|
|
465
|
-
created_at
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
# gate_status is the only enum. `stale` is computed (hash mismatch) but also
|
|
469
|
-
# storable so a detected-stale gate persists across runs.
|
|
470
|
-
gate_enum_values() {
|
|
471
|
-
case "$1" in
|
|
472
|
-
schema_version) printf '%s\n' '1' ;;
|
|
473
|
-
gate_status) printf '%s\n' 'reviewing blocked passed passed-with-tbd stale' ;;
|
|
474
|
-
*) return 1 ;;
|
|
475
|
-
esac
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
# gate_stem_for <prd> — derive the short stem used to name a PRD's gate.yaml
|
|
479
|
-
# and review report. A conforming PRD file is `PRD-<NNN>-<title>.md`, whose
|
|
480
|
-
# gate/report drop the title and key off `PRD-<NNN>` (so one dir with
|
|
481
|
-
# PRD-001/PRD-002 gets PRD-001.gate.yaml / PRD-002.gate.yaml). Non-conforming
|
|
482
|
-
# names (no PRD-NNN prefix) fall back to the full basename so callers still get
|
|
483
|
-
# a stable, collision-free stem instead of an error.
|
|
484
|
-
gate_stem_for() {
|
|
485
|
-
local base stem
|
|
486
|
-
base=$(basename "$1")
|
|
487
|
-
base="${base%.markdown}"
|
|
488
|
-
base="${base%.md}"
|
|
489
|
-
# Avoid a capture group in `[[ =~ ]]`: the old parser in macOS Bash 3.2
|
|
490
|
-
# treats an unquoted `(` as syntax before the conditional can run.
|
|
491
|
-
stem=$(printf '%s\n' "$base" | sed -n 's/^\([Pp][Rr][Dd]-[0-9][0-9]*\).*/\1/p')
|
|
492
|
-
if [ -n "$stem" ]; then
|
|
493
|
-
echo "$stem"
|
|
494
|
-
else
|
|
495
|
-
echo "$base"
|
|
496
|
-
fi
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
# gate_path_for <prd> — echo the gate.yaml path adjacent to the PRD.
|
|
500
|
-
# Named `<stem>.gate.yaml` where stem is PRD-<NNN> (see gate_stem_for).
|
|
501
|
-
gate_path_for() {
|
|
502
|
-
local prd="$1" dir stem
|
|
503
|
-
dir=$(dirname "$prd")
|
|
504
|
-
stem=$(gate_stem_for "$prd")
|
|
505
|
-
echo "${dir}/${stem}.gate.yaml"
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
# gate_report_for <prd> — echo the review report path (PRD same dir).
|
|
509
|
-
# Named `<stem>-REVIEW.md` where stem is PRD-<NNN> (see gate_stem_for).
|
|
510
|
-
gate_report_for() {
|
|
511
|
-
local prd="$1" dir stem
|
|
512
|
-
dir=$(dirname "$prd")
|
|
513
|
-
stem=$(gate_stem_for "$prd")
|
|
514
|
-
echo "${dir}/${stem}-REVIEW.md"
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
# gate_yaml_set <file> <key> <value> — gate-file analogue of yaml_set: validate
|
|
518
|
-
# enum, read the whole file, set one key, rewrite every GATE_FIELD_ORDER field,
|
|
519
|
-
# atomic mv, write-back verify.
|
|
520
|
-
gate_yaml_set() {
|
|
521
|
-
local file="$1" key="$2" value="$3"
|
|
522
|
-
[ -f "$file" ] || die "$COMATE_CORRUPT" "ERROR: gate file not found: $file"
|
|
523
|
-
|
|
524
|
-
local known=0 k
|
|
525
|
-
for k in "${GATE_FIELD_ORDER[@]}"; do
|
|
526
|
-
[ "$k" = "$key" ] && { known=1; break; }
|
|
527
|
-
done
|
|
528
|
-
[ "$known" -eq 1 ] || die "$COMATE_USAGE" "ERROR: unknown gate field '$key'"
|
|
529
|
-
|
|
530
|
-
local legal
|
|
531
|
-
legal=$(gate_enum_values "$key" 2>/dev/null) || legal=""
|
|
532
|
-
if [ -n "$legal" ] && [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
533
|
-
local ok=0 v
|
|
534
|
-
for v in $legal; do [ "$value" = "$v" ] && { ok=1; break; }; done
|
|
535
|
-
[ "$ok" -eq 1 ] || die "$COMATE_USAGE" "ERROR: invalid value '$value' for '$key' (expected: $legal)"
|
|
536
|
-
fi
|
|
537
|
-
|
|
538
|
-
local tmp
|
|
539
|
-
tmp=$(mktemp "${TMPDIR:-/tmp}/comate-gate.XXXXXX") || die "$COMATE_ENV" "ERROR: mktemp failed"
|
|
540
|
-
{
|
|
541
|
-
printf '# comate PRD gate — flat key:value only, no nesting\n'
|
|
542
|
-
for k in "${GATE_FIELD_ORDER[@]}"; do
|
|
543
|
-
local current
|
|
544
|
-
if [ "$k" = "$key" ]; then
|
|
545
|
-
current="$value"
|
|
546
|
-
else
|
|
547
|
-
current=$(yaml_get "$file" "$k")
|
|
548
|
-
fi
|
|
549
|
-
[ -z "$current" ] && current="null"
|
|
550
|
-
printf '%s: %s\n' "$k" "$current"
|
|
551
|
-
done
|
|
552
|
-
} > "$tmp"
|
|
553
|
-
mv "$tmp" "$file" || { rm -f "$tmp"; die "$COMATE_ENV" "ERROR: failed to replace $file"; }
|
|
554
|
-
|
|
555
|
-
local readback; readback=$(yaml_get "$file" "$key")
|
|
556
|
-
if [ "$readback" != "$value" ]; then
|
|
557
|
-
die "$COMATE_CORRUPT" "ERROR: gate write-back mismatch for '$key': wrote '$value', read '$readback'"
|
|
558
|
-
fi
|
|
559
|
-
}
|