@drafthq/draft 2.7.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.
Files changed (158) hide show
  1. package/.claude-plugin/marketplace.json +38 -0
  2. package/.claude-plugin/plugin.json +26 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -0
  5. package/bin/README.md +49 -0
  6. package/cli/bin/draft.js +13 -0
  7. package/cli/src/cli.js +113 -0
  8. package/cli/src/hosts/claude-code.js +46 -0
  9. package/cli/src/hosts/codex.js +33 -0
  10. package/cli/src/hosts/cursor.js +50 -0
  11. package/cli/src/hosts/index.js +24 -0
  12. package/cli/src/hosts/opencode.js +39 -0
  13. package/cli/src/installer.js +61 -0
  14. package/cli/src/lib/fsx.js +34 -0
  15. package/cli/src/lib/graph.js +23 -0
  16. package/cli/src/lib/log.js +32 -0
  17. package/cli/src/lib/paths.js +14 -0
  18. package/core/agents/architect.md +338 -0
  19. package/core/agents/debugger.md +193 -0
  20. package/core/agents/ops.md +104 -0
  21. package/core/agents/planner.md +158 -0
  22. package/core/agents/rca.md +314 -0
  23. package/core/agents/reviewer.md +256 -0
  24. package/core/agents/writer.md +110 -0
  25. package/core/guardrails/README.md +4 -0
  26. package/core/guardrails/code-quality.md +4 -0
  27. package/core/guardrails/dependency-triage.md +4 -0
  28. package/core/guardrails/design-norms.md +4 -0
  29. package/core/guardrails/language-standards.md +4 -0
  30. package/core/guardrails/review-checks.md +4 -0
  31. package/core/guardrails/secure-patterns.md +4 -0
  32. package/core/guardrails/security.md +4 -0
  33. package/core/guardrails.md +22 -0
  34. package/core/knowledge-base.md +127 -0
  35. package/core/methodology.md +1221 -0
  36. package/core/shared/condensation.md +224 -0
  37. package/core/shared/context-verify.md +44 -0
  38. package/core/shared/cross-skill-dispatch.md +127 -0
  39. package/core/shared/discovery-schema.md +75 -0
  40. package/core/shared/draft-context-loading.md +282 -0
  41. package/core/shared/git-report-metadata.md +106 -0
  42. package/core/shared/graph-query.md +239 -0
  43. package/core/shared/graph-usage-report.md +22 -0
  44. package/core/shared/jira-sync.md +170 -0
  45. package/core/shared/parallel-analysis.md +386 -0
  46. package/core/shared/parallel-fanout.md +10 -0
  47. package/core/shared/pattern-learning.md +146 -0
  48. package/core/shared/red-flags.md +58 -0
  49. package/core/shared/template-contract.md +22 -0
  50. package/core/shared/template-hygiene.md +10 -0
  51. package/core/shared/tool-resolver.md +10 -0
  52. package/core/shared/vcs-commands.md +97 -0
  53. package/core/shared/verification-gates.md +47 -0
  54. package/core/templates/CHANGELOG.md +70 -0
  55. package/core/templates/ai-context-export.md +8 -0
  56. package/core/templates/ai-context.md +270 -0
  57. package/core/templates/ai-profile.md +41 -0
  58. package/core/templates/architecture.md +203 -0
  59. package/core/templates/dependency-graph.md +103 -0
  60. package/core/templates/discovery.md +79 -0
  61. package/core/templates/guardrails.md +143 -0
  62. package/core/templates/hld.md +327 -0
  63. package/core/templates/intake-questions.md +403 -0
  64. package/core/templates/jira.md +119 -0
  65. package/core/templates/lld.md +283 -0
  66. package/core/templates/metadata.json +66 -0
  67. package/core/templates/plan.md +130 -0
  68. package/core/templates/product.md +110 -0
  69. package/core/templates/rca.md +86 -0
  70. package/core/templates/root-architecture.md +127 -0
  71. package/core/templates/root-product.md +53 -0
  72. package/core/templates/root-tech-stack.md +117 -0
  73. package/core/templates/service-index.md +55 -0
  74. package/core/templates/session-summary.md +8 -0
  75. package/core/templates/spec.md +165 -0
  76. package/core/templates/tech-matrix.md +101 -0
  77. package/core/templates/tech-stack.md +169 -0
  78. package/core/templates/track-architecture.md +311 -0
  79. package/core/templates/workflow.md +187 -0
  80. package/integrations/agents/AGENTS.md +24384 -0
  81. package/integrations/copilot/.github/copilot-instructions.md +24384 -0
  82. package/integrations/gemini/.gemini.md +26 -0
  83. package/package.json +53 -0
  84. package/scripts/fetch-memory-engine.sh +116 -0
  85. package/scripts/lib.sh +256 -0
  86. package/scripts/tools/_lib.sh +220 -0
  87. package/scripts/tools/adr-index.sh +117 -0
  88. package/scripts/tools/check-graph-usage-report.sh +95 -0
  89. package/scripts/tools/check-scope-conflicts.sh +139 -0
  90. package/scripts/tools/check-skill-line-caps.sh +115 -0
  91. package/scripts/tools/check-template-noop.sh +87 -0
  92. package/scripts/tools/check-track-hygiene.sh +230 -0
  93. package/scripts/tools/classify-files.sh +231 -0
  94. package/scripts/tools/cycle-detect.sh +75 -0
  95. package/scripts/tools/detect-test-framework.sh +135 -0
  96. package/scripts/tools/diff-templates-vs-tracks.sh +176 -0
  97. package/scripts/tools/emit-skill-metrics.sh +71 -0
  98. package/scripts/tools/fix-whitespace.sh +192 -0
  99. package/scripts/tools/freshness-check.sh +143 -0
  100. package/scripts/tools/git-metadata.sh +203 -0
  101. package/scripts/tools/graph-callers.sh +74 -0
  102. package/scripts/tools/graph-impact.sh +93 -0
  103. package/scripts/tools/graph-snapshot.sh +102 -0
  104. package/scripts/tools/hotspot-rank.sh +75 -0
  105. package/scripts/tools/manage-symlinks.sh +85 -0
  106. package/scripts/tools/mermaid-from-graph.sh +92 -0
  107. package/scripts/tools/migrate-track-frontmatter.sh +241 -0
  108. package/scripts/tools/parse-git-log.sh +135 -0
  109. package/scripts/tools/parse-reports.sh +114 -0
  110. package/scripts/tools/render-track.sh +145 -0
  111. package/scripts/tools/run-coverage.sh +153 -0
  112. package/scripts/tools/scan-markers.sh +144 -0
  113. package/scripts/tools/skill-caps.conf +24 -0
  114. package/scripts/tools/validate-frontmatter.sh +125 -0
  115. package/scripts/tools/verify-citations.sh +250 -0
  116. package/scripts/tools/verify-doc-anchors.sh +204 -0
  117. package/scripts/tools/verify-graph-binary.sh +154 -0
  118. package/skills/GRAPH.md +332 -0
  119. package/skills/adr/SKILL.md +374 -0
  120. package/skills/assist-review/SKILL.md +49 -0
  121. package/skills/bughunt/SKILL.md +668 -0
  122. package/skills/bughunt/references/regression-tests.md +399 -0
  123. package/skills/change/SKILL.md +267 -0
  124. package/skills/coverage/SKILL.md +336 -0
  125. package/skills/debug/SKILL.md +201 -0
  126. package/skills/decompose/SKILL.md +656 -0
  127. package/skills/deep-review/SKILL.md +326 -0
  128. package/skills/deploy-checklist/SKILL.md +254 -0
  129. package/skills/discover/SKILL.md +66 -0
  130. package/skills/docs/SKILL.md +42 -0
  131. package/skills/documentation/SKILL.md +197 -0
  132. package/skills/draft/SKILL.md +177 -0
  133. package/skills/draft/context-files.md +57 -0
  134. package/skills/draft/intent-mapping.md +37 -0
  135. package/skills/draft/quality-guide.md +51 -0
  136. package/skills/graph/SKILL.md +107 -0
  137. package/skills/impact/SKILL.md +86 -0
  138. package/skills/implement/SKILL.md +794 -0
  139. package/skills/incident-response/SKILL.md +245 -0
  140. package/skills/index/SKILL.md +848 -0
  141. package/skills/init/SKILL.md +1784 -0
  142. package/skills/init/references/architecture-spec.md +1259 -0
  143. package/skills/integrations/SKILL.md +53 -0
  144. package/skills/jira/SKILL.md +577 -0
  145. package/skills/jira/references/review.md +1322 -0
  146. package/skills/learn/SKILL.md +478 -0
  147. package/skills/new-track/SKILL.md +841 -0
  148. package/skills/ops/SKILL.md +57 -0
  149. package/skills/plan/SKILL.md +60 -0
  150. package/skills/quick-review/SKILL.md +216 -0
  151. package/skills/revert/SKILL.md +178 -0
  152. package/skills/review/SKILL.md +1114 -0
  153. package/skills/standup/SKILL.md +183 -0
  154. package/skills/status/SKILL.md +183 -0
  155. package/skills/tech-debt/SKILL.md +318 -0
  156. package/skills/testing-strategy/SKILL.md +195 -0
  157. package/skills/tour/SKILL.md +38 -0
  158. package/skills/upload/SKILL.md +117 -0
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ # diff-templates-vs-tracks.sh
3
+ #
4
+ # Surface tracks whose artifact set drifts from the current template schema.
5
+ # Used by WS-0 (Templates are the contract) and the deploy-checklist gate.
6
+ #
7
+ # Compares the file set + section headers + required-field count between
8
+ # core/templates/ and each tracks/*/ directory passed on the command line
9
+ # (or every tracks/* found under the current repo if no path is given).
10
+ #
11
+ # Exit codes:
12
+ # 0 no drift
13
+ # 1 drift detected (details on stderr)
14
+ # 2 usage / runtime error
15
+ #
16
+ # Usage:
17
+ # scripts/tools/diff-templates-vs-tracks.sh # scan ./tracks/*
18
+ # scripts/tools/diff-templates-vs-tracks.sh tracks/foo bar/ # scan listed dirs
19
+ # scripts/tools/diff-templates-vs-tracks.sh --json ... # JSON output
20
+ #
21
+ # Notes:
22
+ # - "Drift" is a heuristic: missing required section headers, missing files,
23
+ # or known-removed fields still present.
24
+ # - Templates themselves are linted: a template that omits a header expected
25
+ # by the contract is also flagged.
26
+
27
+ set -euo pipefail
28
+
29
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
30
+ echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
31
+ exit 0
32
+ fi
33
+
34
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ # shellcheck source=/dev/null
36
+ source "$SCRIPT_DIR/_lib.sh"
37
+
38
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
39
+ TEMPLATES_DIR="$REPO_ROOT/core/templates"
40
+
41
+ EMIT_JSON=0
42
+ TRACK_PATHS=()
43
+
44
+ usage() {
45
+ local stream=2 code=2
46
+ if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
47
+ sed -n '2,22p' "$0" >&$stream
48
+ exit "$code"
49
+ }
50
+
51
+ # Argument parsing
52
+ while (($#)); do
53
+ case "$1" in
54
+ -h|--help) USAGE_HELP_MODE=1 usage ;;
55
+ --json) EMIT_JSON=1; shift ;;
56
+ -*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
57
+ *) TRACK_PATHS+=("$1"); shift ;;
58
+ esac
59
+ done
60
+
61
+ if ((${#TRACK_PATHS[@]} == 0)); then
62
+ while IFS= read -r p; do TRACK_PATHS+=("$p"); done < <(discover_track_dirs "$REPO_ROOT")
63
+ fi
64
+
65
+ # Expected artifact files per track (all required at 2.0)
66
+ REQUIRED_FILES=(spec.md plan.md hld.md lld.md metadata.json discovery.md)
67
+
68
+ # Expected required section headers per markdown file. These are heuristics —
69
+ # present in templates at 2.0, must be present in any track that claims to
70
+ # conform. Match is case-insensitive and treats hyphen-or-space as equivalent
71
+ # so author-style variation (Mode Selection vs Mode-selection) doesn't trip
72
+ # the validator. The canonical form is the form in core/templates/.
73
+ required_headers_for() {
74
+ case "$1" in
75
+ spec.md) printf '%s' 'Problem Statement|Requirements|Acceptance Criteria|Risk Assessment|Open Questions' ;;
76
+ plan.md) printf '%s' 'Phase 0|Phase 1|Status Markers' ;;
77
+ hld.md) printf '%s' 'Background|Requirements|High Level Design|Detailed Design|Dependencies|Checklist|Deployment|Observability' ;;
78
+ lld.md) printf '%s' 'Background|Requirements|Low Level Design|Observability' ;;
79
+ discovery.md) printf '%s' 'Hotspots|Mode Selection|Open Questions|References' ;;
80
+ *) printf '%s' '' ;;
81
+ esac
82
+ }
83
+
84
+ # Strings that were removed at 2.0 — should not appear in conforming tracks.
85
+ REMOVED_FIELDS_RE='Author1|xxx@\.com|xxx@example\.com|^Status: \[x\] Complete$'
86
+
87
+ drift_count=0
88
+ declare -a drift_records=()
89
+
90
+ record_drift() {
91
+ local track="$1" kind="$2" detail="$3"
92
+ drift_records+=("$track|$kind|$detail")
93
+ drift_count=$((drift_count + 1))
94
+ }
95
+
96
+ scan_track() {
97
+ local track_dir="$1"
98
+ local rel_track
99
+ rel_track="${track_dir#"$REPO_ROOT/"}"
100
+
101
+ for fname in "${REQUIRED_FILES[@]}"; do
102
+ if [[ ! -f "$track_dir/$fname" ]]; then
103
+ record_drift "$rel_track" "missing-file" "$fname"
104
+ fi
105
+ done
106
+
107
+ for fname in spec.md plan.md hld.md lld.md discovery.md; do
108
+ local fpath="$track_dir/$fname"
109
+ [[ -f "$fpath" ]] || continue
110
+ local pattern
111
+ pattern="$(required_headers_for "$fname")"
112
+ [[ -n "$pattern" ]] || continue
113
+ IFS='|' read -r -a heads <<< "$pattern"
114
+ for h in "${heads[@]}"; do
115
+ # Build a hyphen-or-space-tolerant regex: every literal space in
116
+ # the expected header may match `[ -]` in the actual header.
117
+ local h_flex="${h// /[ -]}"
118
+ # Case-insensitive (-i) substring match against an ATX header line.
119
+ if ! grep -Eiq "^#{1,6} +.*${h_flex}" "$fpath"; then
120
+ record_drift "$rel_track" "missing-header" "$fname:$h"
121
+ fi
122
+ done
123
+ done
124
+
125
+ while IFS= read -r f; do
126
+ local rel_file="${f#"$track_dir/"}"
127
+ if grep -nE "$REMOVED_FIELDS_RE" "$f" >/dev/null 2>&1; then
128
+ while IFS= read -r line; do
129
+ record_drift "$rel_track" "removed-field" "$rel_file:$line"
130
+ done < <(grep -nE "$REMOVED_FIELDS_RE" "$f" | head -5)
131
+ fi
132
+ done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
133
+ }
134
+
135
+ # Sanity-check templates themselves first.
136
+ for fname in "${REQUIRED_FILES[@]}"; do
137
+ [[ -f "$TEMPLATES_DIR/$fname" ]] || record_drift "core/templates" "missing-template" "$fname"
138
+ done
139
+
140
+ for t in "${TRACK_PATHS[@]}"; do
141
+ [[ -d "$t" ]] || { record_drift "$t" "not-a-directory" ""; continue; }
142
+ scan_track "$(cd "$t" && pwd)"
143
+ done
144
+
145
+ emit_records() {
146
+ if ((EMIT_JSON)); then
147
+ printf '{"drift_count": %d, "records": [\n' "$drift_count"
148
+ local first=1
149
+ for r in "${drift_records[@]}"; do
150
+ local track kind detail
151
+ IFS='|' read -r track kind detail <<< "$r"
152
+ if ((first)); then first=0; else printf ',\n'; fi
153
+ printf ' {"track": "%s", "kind": "%s", "detail": "%s"}' \
154
+ "$(json_escape "$track")" \
155
+ "$(json_escape "$kind")" \
156
+ "$(json_escape "$detail")"
157
+ done
158
+ printf '\n]}\n'
159
+ else
160
+ if ((drift_count == 0)); then
161
+ printf 'OK: no drift across %d track(s).\n' "${#TRACK_PATHS[@]}"
162
+ else
163
+ printf 'DRIFT: %d defect(s) across %d track(s).\n' \
164
+ "$drift_count" "${#TRACK_PATHS[@]}" >&2
165
+ local r track kind detail
166
+ for r in "${drift_records[@]}"; do
167
+ IFS='|' read -r track kind detail <<< "$r"
168
+ printf ' [%s] %s — %s\n' "$kind" "$track" "$detail" >&2
169
+ done
170
+ fi
171
+ fi
172
+ }
173
+
174
+ emit_records
175
+
176
+ ((drift_count == 0))
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # emit-skill-metrics.sh — Append a NDJSON metrics record to ~/.draft/metrics.jsonl
3
+ #
4
+ # Usage: emit-skill-metrics.sh <json-payload>
5
+ # json-payload: a JSON object string (must be valid JSON, single line)
6
+ #
7
+ # Exit codes: always 0 (silent on all errors — never break the calling skill)
8
+ # Concurrency: uses flock on the metrics file to prevent interleaved writes
9
+
10
+ set -euo pipefail
11
+
12
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
13
+ cat <<'EOF'
14
+ Usage: emit-skill-metrics.sh '<json-payload>'
15
+
16
+ Appends a single NDJSON record to ~/.draft/metrics.jsonl with an injected
17
+ "ts" field (ISO-8601 UTC). Concurrency-safe via flock. Silent on all errors —
18
+ never fails the calling skill. Rotates the metrics file to the last 1000 lines
19
+ when it exceeds 10MB.
20
+
21
+ Example:
22
+ emit-skill-metrics.sh '{"skill":"review","verdict":"approve"}'
23
+
24
+ Resolution (when invoked by a skill):
25
+ 1. $DRAFT_PLUGIN_ROOT/scripts/tools/emit-skill-metrics.sh
26
+ 2. $HOME/.claude/plugins/draft/scripts/tools/emit-skill-metrics.sh
27
+ 3. $PWD/scripts/tools/emit-skill-metrics.sh
28
+
29
+ Self-test: /draft:draft metrics-check
30
+ EOF
31
+ exit 0
32
+ fi
33
+
34
+ METRICS_DIR="${HOME}/.draft"
35
+ METRICS_FILE="${METRICS_DIR}/metrics.jsonl"
36
+ LOCK_FILE="${METRICS_DIR}/metrics.lock"
37
+
38
+ payload="${1:-}"
39
+
40
+ # Validate that a payload was provided
41
+ if [[ -z "${payload}" ]]; then
42
+ exit 0
43
+ fi
44
+
45
+ # Ensure the metrics directory exists (silent — never fail the caller)
46
+ mkdir -p "${METRICS_DIR}" 2>/dev/null || exit 0
47
+
48
+ # Append ISO timestamp to the payload and write under an exclusive lock
49
+ # flock -x -w 2: acquire exclusive lock, wait up to 2 seconds, then give up
50
+ (
51
+ flock -x -w 2 200 2>/dev/null || exit 0
52
+
53
+ # Inject timestamp into the payload using sed (avoids requiring jq)
54
+ # Assumes payload ends with '}' — insert timestamp field before closing brace
55
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")"
56
+ record="${payload%\}},\"ts\":\"${ts}\"}"
57
+ echo "${record}" >> "${METRICS_FILE}" 2>/dev/null || true
58
+
59
+ # Rotate when file exceeds 10MB: keep last 1000 lines.
60
+ # Cheap size check via `wc -c`; only invoke rotation when triggered.
61
+ if [[ -f "${METRICS_FILE}" ]]; then
62
+ size_bytes=$(wc -c < "${METRICS_FILE}" 2>/dev/null || echo 0)
63
+ if [[ "${size_bytes}" -gt 10485760 ]]; then
64
+ tail -n 1000 "${METRICS_FILE}" > "${METRICS_FILE}.tmp" 2>/dev/null \
65
+ && mv -f "${METRICS_FILE}.tmp" "${METRICS_FILE}" 2>/dev/null \
66
+ || rm -f "${METRICS_FILE}.tmp" 2>/dev/null
67
+ fi
68
+ fi
69
+
70
+ ) 200>"${LOCK_FILE}" 2>/dev/null || true
71
+
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ # fix-whitespace.sh — strip trailing whitespace and blank lines at EOF from
3
+ # AI-generated markdown files.
4
+ #
5
+ # GitHub (and git --check) rejects commits with trailing whitespace or a blank
6
+ # final line. This script normalises draft-generated markdown in-place before
7
+ # the files are committed, preventing upload failures.
8
+ #
9
+ # Usage:
10
+ # # Fix specific files:
11
+ # scripts/tools/fix-whitespace.sh <file> [<file> ...]
12
+ #
13
+ # # Fix all markdown files in a track:
14
+ # scripts/tools/fix-whitespace.sh --track <track_id>
15
+ #
16
+ # # Fix all draft-generated markdown in the repo (safe subset):
17
+ # scripts/tools/fix-whitespace.sh --draft [<repo_root>]
18
+ #
19
+ # Exit codes:
20
+ # 0 — success (all files normalised; prints list of changed files)
21
+ # 1 — invocation error
22
+ # 2 — one or more files could not be processed
23
+
24
+ set -euo pipefail
25
+
26
+ if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
27
+ echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
28
+ exit 0
29
+ fi
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ # shellcheck source=_lib.sh
33
+ source "$SCRIPT_DIR/_lib.sh"
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Internal helpers
37
+ # ---------------------------------------------------------------------------
38
+
39
+ usage() {
40
+ sed -n '2,/^[^#]/p' "$0" | grep '^#' | sed 's/^# \?//'
41
+ }
42
+
43
+ # Fix a single file in-place.
44
+ # Returns: 0 if modified, 1 if already clean (exit code from the function,
45
+ # not the script — caller decides whether to count it).
46
+ fix_file() {
47
+ local file="$1"
48
+ if [[ ! -f "$file" ]]; then
49
+ echo " SKIP (not a file): $file" >&2
50
+ return 2
51
+ fi
52
+ if [[ ! -w "$file" ]]; then
53
+ echo " ERROR (not writable): $file" >&2
54
+ return 2
55
+ fi
56
+
57
+ local original
58
+ original="$(cat "$file")"
59
+
60
+ # 1. Strip trailing whitespace on every line (spaces and tabs).
61
+ # 2. Strip trailing blank lines at EOF, then add exactly one final newline.
62
+ local fixed
63
+ fixed="$(
64
+ printf '%s' "$original" \
65
+ | sed 's/[[:space:]]*$//' \
66
+ | sed -e :a -e '/^\n*$/{$d;N;ba}'
67
+ )"$'\n'
68
+
69
+ if [[ "$fixed" == "$original" ]]; then
70
+ return 1 # already clean
71
+ fi
72
+
73
+ local _tmp
74
+ _tmp="$(mktemp "${file}.XXXXXX")"
75
+ if printf '%s' "$fixed" > "$_tmp"; then
76
+ mv -f "$_tmp" "$file"
77
+ else
78
+ rm -f "$_tmp"
79
+ return 2
80
+ fi
81
+ return 0
82
+ }
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Collect target files from arguments
86
+ # ---------------------------------------------------------------------------
87
+
88
+ TARGETS=()
89
+ REPO_ROOT=""
90
+
91
+ if [[ $# -eq 0 ]]; then
92
+ usage
93
+ exit 0
94
+ fi
95
+
96
+ case "$1" in
97
+ --track)
98
+ [[ $# -ge 2 ]] || { echo "ERROR: --track requires a track_id argument." >&2; exit 1; }
99
+ TRACK_ID="$2"
100
+ # Determine repo root: walk up from cwd until draft/ is found.
101
+ REPO_ROOT="$(pwd)"
102
+ while [[ ! -d "$REPO_ROOT/draft" && "$REPO_ROOT" != "/" ]]; do
103
+ REPO_ROOT="$(dirname "$REPO_ROOT")"
104
+ done
105
+ TRACK_DIR="$REPO_ROOT/draft/tracks/$TRACK_ID"
106
+ if [[ ! -d "$TRACK_DIR" ]]; then
107
+ echo "ERROR: track directory not found: $TRACK_DIR" >&2
108
+ exit 1
109
+ fi
110
+ while IFS= read -r -d '' f; do
111
+ TARGETS+=("$f")
112
+ done < <(find "$TRACK_DIR" -maxdepth 1 -name "*.md" -print0 | sort -z)
113
+ ;;
114
+ --draft)
115
+ REPO_ROOT="${2:-$(pwd)}"
116
+ while [[ ! -d "$REPO_ROOT/draft" && "$REPO_ROOT" != "/" ]]; do
117
+ REPO_ROOT="$(dirname "$REPO_ROOT")"
118
+ done
119
+ if [[ ! -d "$REPO_ROOT/draft" ]]; then
120
+ echo "ERROR: could not locate draft/ directory from: ${2:-$(pwd)}" >&2
121
+ exit 1
122
+ fi
123
+ # Safe subset: only files produced by draft skills.
124
+ while IFS= read -r -d '' f; do
125
+ TARGETS+=("$f")
126
+ done < <(
127
+ find "$REPO_ROOT/draft" \
128
+ -name "architecture.md" \
129
+ -o -name ".ai-context.md" \
130
+ -o -name ".ai-profile.md" \
131
+ -o -name "hld.md" \
132
+ -o -name "lld.md" \
133
+ -o -name "spec.md" \
134
+ -o -name "plan.md" \
135
+ -o -name "rca.md" \
136
+ -o -name "guardrails.md" \
137
+ -o -name "product.md" \
138
+ -o -name "tech-stack.md" \
139
+ | sort \
140
+ | tr '\n' '\0'
141
+ )
142
+ ;;
143
+ -h|--help)
144
+ usage
145
+ exit 0
146
+ ;;
147
+ -*)
148
+ echo "ERROR: unknown option: $1" >&2
149
+ exit 1
150
+ ;;
151
+ *)
152
+ TARGETS=("$@")
153
+ ;;
154
+ esac
155
+
156
+ if [[ ${#TARGETS[@]} -eq 0 ]]; then
157
+ echo "fix-whitespace: no files to process."
158
+ exit 0
159
+ fi
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # Process files
163
+ # ---------------------------------------------------------------------------
164
+
165
+ CHANGED=()
166
+ ERRORS=()
167
+
168
+ for f in "${TARGETS[@]}"; do
169
+ rc=0
170
+ fix_file "$f" || rc=$?
171
+ case $rc in
172
+ 0) CHANGED+=("$f") ;;
173
+ 1) ;; # already clean — silent
174
+ *) ERRORS+=("$f") ;;
175
+ esac
176
+ done
177
+
178
+ if [[ ${#CHANGED[@]} -gt 0 ]]; then
179
+ echo "fix-whitespace: normalised ${#CHANGED[@]} file(s):"
180
+ for f in "${CHANGED[@]}"; do
181
+ echo " $f"
182
+ done
183
+ fi
184
+
185
+ if [[ ${#ERRORS[@]} -gt 0 ]]; then
186
+ echo "fix-whitespace: ERROR — could not process ${#ERRORS[@]} file(s):" >&2
187
+ for f in "${ERRORS[@]}"; do
188
+ echo " $f" >&2
189
+ done
190
+ exit 2
191
+ fi
192
+
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # freshness-check.sh — verify recorded file hashes against current contents.
3
+ #
4
+ # Input state file format (draft/.state/freshness.json):
5
+ # {
6
+ # "generated_at": "2026-04-22T10:00:00Z",
7
+ # "files": [
8
+ # {"path": "draft/architecture.md", "sha256": "abc..."},
9
+ # ...
10
+ # ]
11
+ # }
12
+ #
13
+ # Emits:
14
+ # {
15
+ # "fresh": true|false,
16
+ # "stale_files": ["..."],
17
+ # "missing_files": ["..."],
18
+ # "reason": "..."
19
+ # }
20
+ #
21
+ # Usage:
22
+ # scripts/tools/freshness-check.sh [--state PATH] [--root DIR]
23
+ #
24
+ # Exit codes: 0 fresh, 1 invocation error, 2 stale (still emits JSON).
25
+ set -euo pipefail
26
+
27
+ STATE_FILE=""
28
+ ROOT="."
29
+
30
+ usage() {
31
+ cat <<'EOF'
32
+ freshness-check.sh — verify file hashes against a recorded state snapshot.
33
+
34
+ Usage:
35
+ scripts/tools/freshness-check.sh [--state PATH] [--root DIR]
36
+
37
+ Flags:
38
+ --state PATH Path to freshness JSON (default: <root>/draft/.state/freshness.json).
39
+ --root DIR Repository root to resolve file paths against (default: cwd).
40
+ --help Show this help.
41
+
42
+ Output: JSON {fresh, stale_files, missing_files, reason}.
43
+ Exit 0 fresh, 2 stale (still emits JSON), 1 invocation error.
44
+ EOF
45
+ }
46
+
47
+ while [[ $# -gt 0 ]]; do
48
+ case "$1" in
49
+ --state) STATE_FILE="$2"; shift 2;;
50
+ --root) ROOT="$2"; shift 2;;
51
+ --help|-h) usage; exit 0;;
52
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
53
+ esac
54
+ done
55
+
56
+ if [[ ! -d "$ROOT" ]]; then
57
+ echo "ERROR: --root '$ROOT' is not a directory" >&2
58
+ exit 1
59
+ fi
60
+ ROOT_ABS="$(cd "$ROOT" && pwd)"
61
+
62
+ if [[ -z "$STATE_FILE" ]]; then
63
+ STATE_FILE="$ROOT_ABS/draft/.state/freshness.json"
64
+ fi
65
+
66
+ if [[ ! -f "$STATE_FILE" ]]; then
67
+ esc="${STATE_FILE//\\/\\\\}"; esc="${esc//\"/\\\"}"
68
+ printf '{"fresh": false, "stale_files": [], "missing_files": [], "reason": "no state file at %s"}\n' "$esc"
69
+ exit 2
70
+ fi
71
+
72
+ if ! command -v jq >/dev/null 2>&1; then
73
+ echo "ERROR: jq is required" >&2
74
+ exit 1
75
+ fi
76
+
77
+ # hash calc: prefer sha256sum; fallback shasum -a 256
78
+ sha256() {
79
+ local f="$1"
80
+ if command -v sha256sum >/dev/null 2>&1; then
81
+ sha256sum "$f" | awk '{print $1}'
82
+ elif command -v shasum >/dev/null 2>&1; then
83
+ shasum -a 256 "$f" | awk '{print $1}'
84
+ else
85
+ return 1
86
+ fi
87
+ }
88
+
89
+ stale=()
90
+ missing=()
91
+
92
+ while IFS=$'\t' read -r path expected; do
93
+ [[ -z "$path" ]] && continue
94
+ full="$ROOT_ABS/$path"
95
+ if [[ ! -f "$full" ]]; then
96
+ missing+=("$path")
97
+ continue
98
+ fi
99
+ actual="$(sha256 "$full")"
100
+ if [[ "$actual" != "$expected" ]]; then
101
+ stale+=("$path")
102
+ fi
103
+ done < <(jq -r '.files[]? | [.path, .sha256] | @tsv' "$STATE_FILE")
104
+
105
+ fresh="true"
106
+ reason=""
107
+ if [[ ${#stale[@]} -gt 0 || ${#missing[@]} -gt 0 ]]; then
108
+ fresh="false"
109
+ reason="$([[ ${#stale[@]} -gt 0 ]] && echo "${#stale[@]} stale" || echo "")"
110
+ if [[ ${#missing[@]} -gt 0 ]]; then
111
+ if [[ -n "$reason" ]]; then reason="$reason, "; fi
112
+ reason="${reason}${#missing[@]} missing"
113
+ fi
114
+ fi
115
+
116
+ json_array() {
117
+ local arr=("$@")
118
+ if [[ ${#arr[@]} -eq 0 ]]; then
119
+ printf '[]'
120
+ return
121
+ fi
122
+ printf '['
123
+ local first=true
124
+ for x in "${arr[@]}"; do
125
+ if $first; then first=false; else printf ','; fi
126
+ # Escape quotes.
127
+ escaped="${x//\\/\\\\}"
128
+ escaped="${escaped//\"/\\\"}"
129
+ printf '"%s"' "$escaped"
130
+ done
131
+ printf ']'
132
+ }
133
+
134
+ printf '{"fresh":%s,"stale_files":%s,"missing_files":%s,"reason":"%s"}\n' \
135
+ "$fresh" \
136
+ "$(json_array "${stale[@]+"${stale[@]}"}")" \
137
+ "$(json_array "${missing[@]+"${missing[@]}"}")" \
138
+ "$reason"
139
+
140
+ if [[ "$fresh" == "false" ]]; then
141
+ exit 2
142
+ fi
143
+ exit 0