qualia-framework 6.3.0 → 6.4.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 (44) hide show
  1. package/AGENTS.md +8 -8
  2. package/CLAUDE.md +5 -5
  3. package/README.md +17 -39
  4. package/bin/cli.js +64 -16
  5. package/bin/command-surface.js +5 -1
  6. package/bin/install.js +26 -11
  7. package/bin/learning-candidates.js +217 -0
  8. package/bin/prune-deprecated.js +64 -0
  9. package/bin/runtime-manifest.js +4 -0
  10. package/bin/security-scan.js +409 -0
  11. package/bin/status-snapshot.js +363 -0
  12. package/guide.md +11 -33
  13. package/hooks/pre-compact.js +232 -0
  14. package/package.json +1 -1
  15. package/skills/qualia/SKILL.md +1 -1
  16. package/skills/qualia-build/SKILL.md +1 -1
  17. package/skills/qualia-discuss/SKILL.md +1 -1
  18. package/skills/qualia-doctor/SKILL.md +1 -1
  19. package/skills/qualia-feature/SKILL.md +1 -1
  20. package/skills/qualia-fix/SKILL.md +1 -1
  21. package/skills/qualia-idk/SKILL.md +245 -0
  22. package/skills/qualia-learn/SKILL.md +1 -1
  23. package/skills/qualia-map/SKILL.md +1 -1
  24. package/skills/qualia-milestone/SKILL.md +1 -1
  25. package/skills/qualia-new/SKILL.md +1 -1
  26. package/skills/qualia-optimize/SKILL.md +1 -1
  27. package/skills/qualia-plan/SKILL.md +1 -1
  28. package/skills/qualia-polish/SKILL.md +1 -1
  29. package/skills/qualia-postmortem/SKILL.md +1 -1
  30. package/skills/qualia-report/SKILL.md +1 -1
  31. package/skills/qualia-research/SKILL.md +1 -1
  32. package/skills/qualia-review/SKILL.md +1 -1
  33. package/skills/qualia-road/SKILL.md +1 -1
  34. package/skills/qualia-secure/SKILL.md +105 -0
  35. package/skills/qualia-test/SKILL.md +1 -1
  36. package/skills/qualia-verify/SKILL.md +1 -1
  37. package/skills/zoho-workflow/SKILL.md +1 -1
  38. package/tests/bin.test.sh +9 -9
  39. package/tests/install-smoke.test.sh +3 -3
  40. package/tests/lib.test.sh +6 -6
  41. package/tests/published-install-smoke.test.sh +3 -3
  42. package/tests/refs.test.sh +29 -22
  43. package/tests/runner.js +3 -3
  44. package/tests/state.test.sh +38 -7
package/tests/lib.test.sh CHANGED
@@ -369,9 +369,9 @@ CS="$FRAMEWORK_DIR/bin/command-surface.js"
369
369
  $NODE --check "$CS" >/dev/null 2>&1 && ok "command-surface.js parses" || fail "command-surface.js parse"
370
370
  RES=$($NODE -e '
371
371
  const cs = require("'"$CS"'");
372
- console.log(cs.ACTIVE_SKILLS.length === 23 && cs.RETIRED_SKILLS.includes("qualia-debug") && cs.RETIRED_SKILLS.includes("qualia-vibe") ? "SURFACE-OK" : JSON.stringify(cs));
372
+ console.log(cs.ACTIVE_SKILLS.length === 25 && cs.ACTIVE_SKILLS.includes("qualia-idk") && cs.ACTIVE_SKILLS.includes("qualia-secure") && cs.RETIRED_SKILLS.includes("qualia-debug") && cs.RETIRED_SKILLS.includes("qualia-vibe") ? "SURFACE-OK" : JSON.stringify(cs));
373
373
  ')
374
- [ "$RES" = "SURFACE-OK" ] && ok "command-surface defines 23 active skills and retired folds" || fail "command-surface manifest: $RES"
374
+ [ "$RES" = "SURFACE-OK" ] && ok "command-surface defines 25 active skills (incl qualia-idk + qualia-secure) and retired folds" || fail "command-surface manifest: $RES"
375
375
  RES=$($NODE -e '
376
376
  const fs = require("fs");
377
377
  const path = require("path");
@@ -500,7 +500,7 @@ TMP=$(mktmp)
500
500
  mkdir -p "$TMP/home/.claude/bin" "$TMP/home/.claude/hooks" "$TMP/home/.claude/knowledge/daily-log" "$TMP/home/.claude/qualia-design" "$TMP/home/.claude/agents" "$TMP/home/.claude/qualia-templates" "$TMP/project"
501
501
  echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
502
502
  touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
503
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js; do
503
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js; do
504
504
  touch "$TMP/home/.claude/bin/$f"
505
505
  done
506
506
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
@@ -510,7 +510,7 @@ touch "$TMP/home/.claude/knowledge/index.md" "$TMP/home/.claude/knowledge/agents
510
510
  for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
511
511
  touch "$TMP/home/.claude/qualia-design/$f"
512
512
  done
513
- for s in qualia qualia-new qualia-discuss qualia-map qualia-research qualia-plan qualia-build qualia-verify qualia-fix qualia-feature qualia-review qualia-optimize qualia-polish qualia-test qualia-milestone qualia-ship qualia-handoff qualia-report qualia-doctor qualia-road qualia-learn qualia-postmortem zoho-workflow; do
513
+ for s in qualia qualia-new qualia-discuss qualia-map qualia-research qualia-plan qualia-build qualia-verify qualia-fix qualia-feature qualia-review qualia-optimize qualia-polish qualia-test qualia-milestone qualia-ship qualia-handoff qualia-report qualia-doctor qualia-road qualia-learn qualia-postmortem qualia-idk qualia-secure zoho-workflow; do
514
514
  mkdir -p "$TMP/home/.claude/skills/$s"
515
515
  touch "$TMP/home/.claude/skills/$s/SKILL.md"
516
516
  done
@@ -615,7 +615,7 @@ TMP=$(mktmp)
615
615
  mkdir -p "$TMP/.claude/bin" "$TMP/.claude/hooks" "$TMP/.claude/knowledge/daily-log" "$TMP/.claude/qualia-design" "$TMP/.claude/agents" "$TMP/.claude/qualia-templates" "$TMP/project/.planning"
616
616
  echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
617
617
  touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
618
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js; do
618
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js; do
619
619
  touch "$TMP/.claude/bin/$f"
620
620
  done
621
621
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
@@ -625,7 +625,7 @@ touch "$TMP/.claude/knowledge/index.md" "$TMP/.claude/knowledge/agents.md"
625
625
  for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
626
626
  touch "$TMP/.claude/qualia-design/$f"
627
627
  done
628
- for s in qualia qualia-new qualia-discuss qualia-map qualia-research qualia-plan qualia-build qualia-verify qualia-fix qualia-feature qualia-review qualia-optimize qualia-polish qualia-test qualia-milestone qualia-ship qualia-handoff qualia-report qualia-doctor qualia-road qualia-learn qualia-postmortem zoho-workflow; do
628
+ for s in qualia qualia-new qualia-discuss qualia-map qualia-research qualia-plan qualia-build qualia-verify qualia-fix qualia-feature qualia-review qualia-optimize qualia-polish qualia-test qualia-milestone qualia-ship qualia-handoff qualia-report qualia-doctor qualia-road qualia-learn qualia-postmortem qualia-idk qualia-secure zoho-workflow; do
629
629
  mkdir -p "$TMP/.claude/skills/$s"
630
630
  touch "$TMP/.claude/skills/$s/SKILL.md"
631
631
  done
@@ -102,10 +102,10 @@ else
102
102
  fi
103
103
 
104
104
  if [ -d "$HOME_DIR/.claude/hooks" ] \
105
- && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "12" ] \
105
+ && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "13" ] \
106
106
  && [ -f "$HOME_DIR/.claude/hooks/fawzi-approval-guard.js" ] \
107
- && [ ! -f "$HOME_DIR/.claude/hooks/pre-compact.js" ]; then
108
- pass "public install has 12 hooks and no pre-compact"
107
+ && [ -f "$HOME_DIR/.claude/hooks/pre-compact.js" ]; then
108
+ pass "public install has 13 hooks including pre-compact (v6.3.2 sidecar snapshot)"
109
109
  else
110
110
  fail_case "public install hook set mismatch"
111
111
  fi
@@ -79,27 +79,33 @@ SCAN_FILES=$(
79
79
  # 1. `/qualia-foo` — backticked, the canonical command-doc style
80
80
  # 2. <dt>/qualia-foo</dt> — HTML help/onboarding pages
81
81
  # We capture name + file:line so we can show context per failure.
82
- declare -A SEEN_REFS
83
- declare -A REF_LOCATIONS
84
- declare -A ACTIVE_SKILL_MAP
85
-
86
- while IFS= read -r skill; do
87
- [ -n "$skill" ] && ACTIVE_SKILL_MAP["/$skill"]=1
88
- done < <("$NODE" -e 'const {ACTIVE_SKILLS}=require(process.argv[1]); for (const s of ACTIVE_SKILLS) console.log(s)' "$FRAMEWORK_ROOT/bin/command-surface.js")
82
+ #
83
+ # Storage uses a temp file (one line per "ref<TAB>location") instead of
84
+ # bash 4 associative arrays — macOS ships bash 3.2 (frozen at that version
85
+ # for licensing reasons) which lacks `declare -A`. Temp file is portable
86
+ # and behaves correctly under `set -u`.
87
+ REF_STORE=$(mktemp)
88
+ ACTIVE_SKILLS_LIST=$("$NODE" -e 'const {ACTIVE_SKILLS}=require(process.argv[1]); console.log(ACTIVE_SKILLS.map(s => "/" + s).join(" "))' "$FRAMEWORK_ROOT/bin/command-surface.js")
89
+ trap 'rm -f "$REF_STORE"' EXIT
90
+
91
+ # is_active "/qualia-foo" → returns 0 if in ACTIVE_SKILLS_LIST.
92
+ is_active() {
93
+ case " $ACTIVE_SKILLS_LIST " in
94
+ *" $1 "*) return 0 ;;
95
+ *) return 1 ;;
96
+ esac
97
+ }
89
98
 
90
99
  while IFS= read -r file; do
91
100
  # Pattern A: backtick-quoted commands. Allow trailing flag/word but only capture base name.
92
101
  while IFS=: read -r path lineno line; do
93
102
  [ -z "$line" ] && continue
94
- # Skip migration-context lines.
95
103
  if echo "$line" | grep -qE "$MIGRATION_CONTEXT_REGEX"; then
96
104
  continue
97
105
  fi
98
- # Extract every backticked /qualia-foo on this line.
99
106
  matches=$(echo "$line" | grep -oE '`/qualia(-[a-z]+){0,3}`' | sed 's/^`//; s/`$//')
100
107
  for ref in $matches; do
101
- SEEN_REFS["$ref"]=1
102
- REF_LOCATIONS["$ref"]="${REF_LOCATIONS[$ref]:+${REF_LOCATIONS[$ref]}, }$(basename "$path"):$lineno"
108
+ printf '%s\t%s:%s\n' "$ref" "$(basename "$path")" "$lineno" >> "$REF_STORE"
103
109
  done
104
110
  done < <(grep -nE '`/qualia(-[a-z]+){0,3}`' "$file" 2>/dev/null)
105
111
 
@@ -111,36 +117,37 @@ while IFS= read -r file; do
111
117
  fi
112
118
  matches=$(echo "$line" | grep -oE '<dt>/qualia(-[a-z]+){0,3}( [^<]*)?</dt>' | sed -E 's|<dt>(/qualia(-[a-z]+){0,3}).*|\1|')
113
119
  for ref in $matches; do
114
- SEEN_REFS["$ref"]=1
115
- REF_LOCATIONS["$ref"]="${REF_LOCATIONS[$ref]:+${REF_LOCATIONS[$ref]}, }$(basename "$path"):$lineno"
120
+ printf '%s\t%s:%s\n' "$ref" "$(basename "$path")" "$lineno" >> "$REF_STORE"
116
121
  done
117
122
  done < <(grep -nE '<dt>/qualia' "$file" 2>/dev/null)
118
123
  done <<<"$SCAN_FILES"
119
124
 
120
- if [ ${#SEEN_REFS[@]} -eq 0 ]; then
125
+ if [ ! -s "$REF_STORE" ]; then
121
126
  fail_case "scan" "no backticked /qualia-* references found across active surfaces (scanner broken?)"
122
127
  echo ""
123
128
  echo "Results: $PASS passed, $FAIL failed"
124
129
  exit 1
125
130
  fi
126
131
 
127
- # Sort refs for deterministic output.
128
- for ref in $(printf '%s\n' "${!SEEN_REFS[@]}" | sort); do
132
+ # Iterate unique refs, deterministic order.
133
+ for ref in $(cut -f1 "$REF_STORE" | sort -u); do
129
134
  name="${ref#/}"
130
135
  skill_dir="$SKILLS_DIR/$name"
131
- locations="${REF_LOCATIONS[$ref]}"
132
- if [ "${ACTIVE_SKILL_MAP[$ref]:-}" = "1" ] && [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ]; then
136
+ # Comma-join all locations for this ref.
137
+ locations=$(awk -v r="$ref" -F'\t' '$1==r {print $2}' "$REF_STORE" | paste -sd, -)
138
+ if is_active "$ref" && [ -d "$skill_dir" ] && [ -f "$skill_dir/SKILL.md" ]; then
133
139
  pass "$ref → active skills/$name/SKILL.md"
134
140
  continue
135
141
  fi
136
142
  fail_case "$ref" "not an active shipped command or missing skills/$name/SKILL.md — referenced by: $locations"
137
143
  done
138
144
 
139
- PACKAGE_VERSION="$("$NODE" -e 'console.log(require(process.argv[1]).version)' "$FRAMEWORK_ROOT/package.json" 2>/dev/null || echo "")"
140
- if [ -n "$PACKAGE_VERSION" ] && grep -q "# Qualia Framework v$PACKAGE_VERSION" "$FRAMEWORK_ROOT/README.md"; then
141
- pass "README title matches package.json version ($PACKAGE_VERSION)"
145
+ # README title is version-less by design — version lives in package.json + CHANGELOG.md,
146
+ # not in the README h1, so the orientation block above the fold stays version-stable.
147
+ if grep -qE '^# Qualia Framework( |$)' "$FRAMEWORK_ROOT/README.md"; then
148
+ pass "README h1 is version-less (version lives in CHANGELOG.md)"
142
149
  else
143
- fail_case "README version drift" "README.md title must match package.json version $PACKAGE_VERSION"
150
+ fail_case "README h1 shape" "README.md must open with '# Qualia Framework' (no version suffix)"
144
151
  fi
145
152
 
146
153
  forbidden_surface_patterns=(
package/tests/runner.js CHANGED
@@ -2578,14 +2578,14 @@ describe("install.js", () => {
2578
2578
  }
2579
2579
  });
2580
2580
 
2581
- it("12 hooks installed (v6.2.11: proxy approval guard added; v6.2.0: pre-compact removed)", () => {
2581
+ it("13 hooks installed (v6.3.2: pre-compact reintroduced with sidecar-snapshot mechanism)", () => {
2582
2582
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2583
2583
  try {
2584
2584
  runInstall("QS-FAWZI-11", tmpHome);
2585
2585
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2586
- assert.equal(hooks.length, 12, `expected 12 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2586
+ assert.equal(hooks.length, 13, `expected 13 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2587
2587
  assert.ok(hooks.includes("fawzi-approval-guard.js"), "fawzi-approval-guard.js must be installed");
2588
- assert.ok(!hooks.includes("pre-compact.js"), "pre-compact.js must NOT be installed (removed in v6.2.0)");
2588
+ assert.ok(hooks.includes("pre-compact.js"), "pre-compact.js must be installed (v6.3.2 sidecar-snapshot mechanism)");
2589
2589
  } finally {
2590
2590
  fs.rmSync(tmpHome, { recursive: true, force: true });
2591
2591
  }
@@ -4,9 +4,40 @@
4
4
 
5
5
  PASS=0
6
6
  FAIL=0
7
- # Resolve STATE_JS to an ABSOLUTE path so `cd` inside subshells doesn't break it.
8
- STATE_JS="$(cd "$(dirname "$0")/../bin" && pwd)/state.js"
9
- FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
7
+ # Windows portability coerce shell-native MSYS paths (/d/a/..., /tmp/...) to
8
+ # Windows-native form (D:/a/..., C:/Users/.../Temp/...) before they get
9
+ # interpolated into Node `$NODE -e` snippets. Without coercion, Node receives
10
+ # the MSYS string literally and resolves it against the current drive, yielding
11
+ # non-existent paths like `D:\tmp\tmp.XXX\.planning\tracking.json`.
12
+ #
13
+ # Mechanism: `cygpath -m` is the canonical MSYS path converter and is present
14
+ # on Git Bash / MSYS2 (Windows GitHub runners use Git Bash for `shell: bash`).
15
+ # `pwd -W` doesn't work because bash's builtin pwd doesn't accept -W, and
16
+ # /usr/bin/pwd's -W support varies by MSYS distribution.
17
+ # On Linux/macOS, cygpath doesn't exist — fall through to plain pwd.
18
+ _pwd_native() {
19
+ local p
20
+ p=$(pwd)
21
+ if command -v cygpath >/dev/null 2>&1; then
22
+ cygpath -m "$p" 2>/dev/null || echo "$p"
23
+ else
24
+ echo "$p"
25
+ fi
26
+ }
27
+ _mktemp_native() {
28
+ local d
29
+ d=$(mktemp -d) || return 1
30
+ if command -v cygpath >/dev/null 2>&1; then
31
+ cygpath -m "$d" 2>/dev/null || echo "$d"
32
+ else
33
+ echo "$d"
34
+ fi
35
+ }
36
+
37
+ # Resolve STATE_JS to an ABSOLUTE Windows-native path so `cd` inside subshells
38
+ # doesn't break it.
39
+ STATE_JS="$(cd "$(dirname "$0")/../bin" && _pwd_native)/state.js"
40
+ FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && _pwd_native)"
10
41
  STATE_LEDGER_JS="$FRAMEWORK_DIR/bin/state-ledger.js"
11
42
  NODE="${NODE:-node}"
12
43
 
@@ -23,7 +54,7 @@ trap cleanup EXIT
23
54
  # Prints the absolute path to the new tmp dir (does NOT cd).
24
55
  make_project() {
25
56
  local TMP
26
- TMP=$(mktemp -d)
57
+ TMP=$(_mktemp_native)
27
58
  TMP_DIRS+=("$TMP")
28
59
  (
29
60
  cd "$TMP" || exit 1
@@ -129,7 +160,7 @@ fi
129
160
  echo "basic I/O:"
130
161
 
131
162
  # 1. cmdInit produces valid tracking.json + STATE.md
132
- TMP=$(mktemp -d); TMP_DIRS+=("$TMP")
163
+ TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
133
164
  (
134
165
  cd "$TMP" || exit 1
135
166
  $NODE "$STATE_JS" init \
@@ -190,7 +221,7 @@ else
190
221
  fi
191
222
 
192
223
  # 3. cmdCheck with no project → ok:false NO_PROJECT, exit 1
193
- TMP2=$(mktemp -d); TMP_DIRS+=("$TMP2")
224
+ TMP2=$(_mktemp_native); TMP_DIRS+=("$TMP2")
194
225
  OUT=$(cd "$TMP2" && $NODE "$STATE_JS" check 2>&1)
195
226
  CHECK_EXIT=$?
196
227
  if [ "$CHECK_EXIT" -eq 1 ] \
@@ -1014,7 +1045,7 @@ else
1014
1045
  fi
1015
1046
 
1016
1047
  # 49. First-time init (no existing tracking.json) sets lifetime to zeros
1017
- TMP=$(mktemp -d); TMP_DIRS+=("$TMP")
1048
+ TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
1018
1049
  (cd "$TMP" && $NODE "$STATE_JS" init \
1019
1050
  --project "FreshProject" \
1020
1051
  --phases '[{"name":"P1","goal":"G1"}]' \