@a-company/paradigm 3.1.0 → 3.1.4

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.
@@ -4,100 +4,8 @@
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  import chalk from "chalk";
7
- var POST_COMMIT_HOOK = `#!/bin/sh
8
- # Paradigm post-commit hook - captures history from commits
9
- # Installed by: paradigm hooks install
10
-
11
- # Get the commit message and hash
12
- COMMIT_HASH=$(git rev-parse HEAD)
13
- COMMIT_MSG=$(git log -1 --pretty=%B)
14
-
15
- # Get changed files
16
- CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD)
17
-
18
- # Extract symbols from changed files (look for .purpose files)
19
- extract_symbols() {
20
- local symbols=""
21
- for file in $CHANGED_FILES; do
22
- # Check if there's a .purpose file in the directory
23
- dir=$(dirname "$file")
24
- while [ "$dir" != "." ]; do
25
- if [ -f "$dir/.purpose" ]; then
26
- # Extract feature/component names from .purpose
27
- purpose_symbols=$(grep -E '^(features|components|gates|flows):' "$dir/.purpose" -A 10 2>/dev/null | grep -E '^ - (name|id):' | sed 's/.*: //' | tr '\\n' ',' | sed 's/,$//')
28
- if [ -n "$purpose_symbols" ]; then
29
- symbols="$symbols,$purpose_symbols"
30
- fi
31
- break
32
- fi
33
- dir=$(dirname "$dir")
34
- done
35
- done
36
- echo "$symbols" | sed 's/^,//' | tr ',' '\\n' | sort -u | tr '\\n' ',' | sed 's/,$//'
37
- }
38
-
39
- SYMBOLS=$(extract_symbols)
40
-
41
- # Extract symbols from commit message Symbols: trailer
42
- MSG_SYMBOLS=$(echo "$COMMIT_MSG" | grep -E '^Symbols:' | sed 's/^Symbols: //' | tr -d ' ')
43
- if [ -n "$MSG_SYMBOLS" ]; then
44
- if [ -n "$SYMBOLS" ]; then
45
- SYMBOLS="$SYMBOLS,$MSG_SYMBOLS"
46
- else
47
- SYMBOLS="$MSG_SYMBOLS"
48
- fi
49
- # Deduplicate
50
- SYMBOLS=$(echo "$SYMBOLS" | tr ',' '\\n' | sort -u | tr '\\n' ',' | sed 's/,$//')
51
- fi
52
-
53
- # Determine intent from commit message
54
- determine_intent() {
55
- case "$COMMIT_MSG" in
56
- feat*|feature*|add*) echo "feature" ;;
57
- fix*|bug*) echo "fix" ;;
58
- refactor*) echo "refactor" ;;
59
- *) echo "feature" ;;
60
- esac
61
- }
62
-
63
- INTENT=$(determine_intent)
64
-
65
- # Record if we found symbols (from .purpose or commit message) and .paradigm/history exists
66
- if [ -n "$SYMBOLS" ] && [ -d ".paradigm/history" ]; then
67
- # Generate entry ID
68
- if [ -f ".paradigm/history/log.jsonl" ]; then
69
- COUNT=$(wc -l < ".paradigm/history/log.jsonl" | tr -d ' ')
70
- COUNT=$((COUNT + 1))
71
- else
72
- COUNT=1
73
- fi
74
- ID=$(printf "h%04d" $COUNT)
75
-
76
- # Create entry
77
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
78
- AUTHOR=$(git config user.name || echo "unknown")
79
-
80
- # Format symbols as JSON array
81
- SYMBOLS_JSON=$(echo "$SYMBOLS" | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')
82
7
 
83
- # Format files as JSON array
84
- FILES_JSON=$(echo "$CHANGED_FILES" | tr '\\n' ',' | sed 's/,$//' | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')
85
-
86
- # Write entry
87
- echo "{\\"id\\":\\"$ID\\",\\"ts\\":\\"$TIMESTAMP\\",\\"type\\":\\"implement\\",\\"symbols\\":[$SYMBOLS_JSON],\\"author\\":{\\"type\\":\\"human\\",\\"id\\":\\"$AUTHOR\\"},\\"commit\\":\\"$COMMIT_HASH\\",\\"intent\\":\\"$INTENT\\",\\"files\\":[$FILES_JSON],\\"description\\":\\"$(echo "$COMMIT_MSG" | head -1 | sed 's/"/\\\\"/g')\\"}" >> .paradigm/history/log.jsonl
88
-
89
- echo "[paradigm] History entry $ID recorded"
90
- fi
91
- `;
92
- var PRE_PUSH_HOOK = `#!/bin/sh
93
- # Paradigm pre-push hook - reindex history before pushing
94
- # Installed by: paradigm hooks install
95
-
96
- if [ -d ".paradigm/history" ] && [ -f ".paradigm/history/log.jsonl" ]; then
97
- echo "[paradigm] Reindexing history..."
98
- npx paradigm history reindex 2>/dev/null || true
99
- fi
100
- `;
8
+ // src/commands/hooks/generated-hooks.ts
101
9
  var CLAUDE_CODE_STOP_HOOK = `#!/bin/sh
102
10
  # Paradigm Claude Code Stop Hook (v2)
103
11
  # Validates paradigm compliance before allowing the agent to finish.
@@ -251,8 +159,8 @@ for purpose_file in $(find . -name ".purpose" -not -path "*/node_modules/*" -not
251
159
  if [ "$in_anchors" = true ]; then
252
160
  anchor_path=$(echo "$line" | sed 's/.*- //' | sed 's/:.*//' | tr -d ' ')
253
161
  if [ -n "$anchor_path" ]; then
254
- resolved_path="$purpose_dir/$anchor_path"
255
- if [ ! -f "$resolved_path" ]; then
162
+ # Try relative to .purpose dir first, then project root
163
+ if [ ! -f "$purpose_dir/$anchor_path" ] && [ ! -f "./$anchor_path" ]; then
256
164
  VIOLATIONS="$VIOLATIONS
257
165
  - Aspect anchor '$anchor_path' in $purpose_file does not exist.
258
166
  Update the anchor or remove the stale aspect."
@@ -357,6 +265,13 @@ if [ "$SOURCE_COUNT" -ge 3 ] && [ -d ".paradigm/lore" ]; then
357
265
  fi
358
266
  fi
359
267
 
268
+ # --- Auto-evaluate on-stop habits via CLI ---
269
+ if command -v paradigm >/dev/null 2>&1; then
270
+ paradigm habits check --trigger on-stop --record --json 2>/dev/null || true
271
+ elif command -v npx >/dev/null 2>&1; then
272
+ npx paradigm habits check --trigger on-stop --record --json 2>/dev/null || true
273
+ fi
274
+
360
275
  # --- Check 8: Blocking habits ---
361
276
  if [ -f ".paradigm/.habits-blocking" ]; then
362
277
  HABITS_BLOCKING=$(cat ".paradigm/.habits-blocking")
@@ -561,8 +476,8 @@ var CURSOR_STOP_HOOK = `#!/bin/sh
561
476
  # 4. Aspect anchor files that no longer exist
562
477
  # 5. Per-directory .purpose freshness (tracked via .pending-review)
563
478
  # 6. Aspect coverage advisory
564
- # 8. Blocking habits not satisfied (from paradigm_habits_check)
565
479
  # 7. Lore entry expected for significant sessions (3+ source files)
480
+ # 8. Blocking habits not satisfied (from paradigm_habits_check)
566
481
 
567
482
  # Read JSON from stdin (hook input)
568
483
  INPUT=$(cat)
@@ -571,7 +486,7 @@ INPUT=$(cat)
571
486
  if command -v jq >/dev/null 2>&1; then
572
487
  CWD=$(echo "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
573
488
  else
574
- CWD=$(echo "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\\\\["[^"]*"' | head -1 | sed 's/.*\\\\["//' | sed 's/"$//')
489
+ CWD=$(echo "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\\["[^"]*"' | head -1 | sed 's/.*\\["//' | sed 's/"$//')
575
490
  fi
576
491
 
577
492
  if [ -z "$CWD" ]; then
@@ -670,7 +585,7 @@ else
670
585
  case "$file" in
671
586
  *.ts|*.js|*.tsx|*.jsx|*.py|*.rs|*.go)
672
587
  if [ -f "$file" ]; then
673
- if grep -qE '\\\\.(get|post|put|patch|delete)\\\\s*\\\\(|router\\\\.|app\\\\.(get|post|put|delete)|@(Get|Post|Put|Delete)|#\\\\[actix_web::(get|post)' "$file" 2>/dev/null; then
588
+ if grep -qE '\\.(get|post|put|patch|delete)\\s*\\(|router\\.|app\\.(get|post|put|delete)|@(Get|Post|Put|Delete)|#\\[actix_web::(get|post)' "$file" 2>/dev/null; then
674
589
  ROUTE_FILES="$ROUTE_FILES $file"
675
590
  fi
676
591
  fi
@@ -699,8 +614,8 @@ for purpose_file in $(find . -name ".purpose" -not -path "*/node_modules/*" -not
699
614
  if [ "$in_anchors" = true ]; then
700
615
  anchor_path=$(echo "$line" | sed 's/.*- //' | sed 's/:.*//' | tr -d ' ')
701
616
  if [ -n "$anchor_path" ]; then
702
- resolved_path="$purpose_dir/$anchor_path"
703
- if [ ! -f "$resolved_path" ]; then
617
+ # Try relative to .purpose dir first, then project root
618
+ if [ ! -f "$purpose_dir/$anchor_path" ] && [ ! -f "./$anchor_path" ]; then
704
619
  VIOLATIONS="$VIOLATIONS
705
620
  - Aspect anchor '$anchor_path' in $purpose_file does not exist.
706
621
  Update the anchor or remove the stale aspect."
@@ -805,6 +720,13 @@ if [ "$SOURCE_COUNT" -ge 3 ] && [ -d ".paradigm/lore" ]; then
805
720
  fi
806
721
  fi
807
722
 
723
+ # --- Auto-evaluate on-stop habits via CLI ---
724
+ if command -v paradigm >/dev/null 2>&1; then
725
+ paradigm habits check --trigger on-stop --record --json 2>/dev/null || true
726
+ elif command -v npx >/dev/null 2>&1; then
727
+ npx paradigm habits check --trigger on-stop --record --json 2>/dev/null || true
728
+ fi
729
+
808
730
  # --- Check 8: Blocking habits ---
809
731
  if [ -f ".paradigm/.habits-blocking" ]; then
810
732
  HABITS_BLOCKING=$(cat ".paradigm/.habits-blocking")
@@ -994,6 +916,102 @@ done
994
916
  # Never block \u2014 exit 0
995
917
  exit 0
996
918
  `;
919
+
920
+ // src/commands/hooks/index.ts
921
+ var POST_COMMIT_HOOK = `#!/bin/sh
922
+ # Paradigm post-commit hook - captures history from commits
923
+ # Installed by: paradigm hooks install
924
+
925
+ # Get the commit message and hash
926
+ COMMIT_HASH=$(git rev-parse HEAD)
927
+ COMMIT_MSG=$(git log -1 --pretty=%B)
928
+
929
+ # Get changed files
930
+ CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD)
931
+
932
+ # Extract symbols from changed files (look for .purpose files)
933
+ extract_symbols() {
934
+ local symbols=""
935
+ for file in $CHANGED_FILES; do
936
+ # Check if there's a .purpose file in the directory
937
+ dir=$(dirname "$file")
938
+ while [ "$dir" != "." ]; do
939
+ if [ -f "$dir/.purpose" ]; then
940
+ # Extract feature/component names from .purpose
941
+ purpose_symbols=$(grep -E '^(features|components|gates|flows):' "$dir/.purpose" -A 10 2>/dev/null | grep -E '^ - (name|id):' | sed 's/.*: //' | tr '\\n' ',' | sed 's/,$//')
942
+ if [ -n "$purpose_symbols" ]; then
943
+ symbols="$symbols,$purpose_symbols"
944
+ fi
945
+ break
946
+ fi
947
+ dir=$(dirname "$dir")
948
+ done
949
+ done
950
+ echo "$symbols" | sed 's/^,//' | tr ',' '\\n' | sort -u | tr '\\n' ',' | sed 's/,$//'
951
+ }
952
+
953
+ SYMBOLS=$(extract_symbols)
954
+
955
+ # Extract symbols from commit message Symbols: trailer
956
+ MSG_SYMBOLS=$(echo "$COMMIT_MSG" | grep -E '^Symbols:' | sed 's/^Symbols: //' | tr -d ' ')
957
+ if [ -n "$MSG_SYMBOLS" ]; then
958
+ if [ -n "$SYMBOLS" ]; then
959
+ SYMBOLS="$SYMBOLS,$MSG_SYMBOLS"
960
+ else
961
+ SYMBOLS="$MSG_SYMBOLS"
962
+ fi
963
+ # Deduplicate
964
+ SYMBOLS=$(echo "$SYMBOLS" | tr ',' '\\n' | sort -u | tr '\\n' ',' | sed 's/,$//')
965
+ fi
966
+
967
+ # Determine intent from commit message
968
+ determine_intent() {
969
+ case "$COMMIT_MSG" in
970
+ feat*|feature*|add*) echo "feature" ;;
971
+ fix*|bug*) echo "fix" ;;
972
+ refactor*) echo "refactor" ;;
973
+ *) echo "feature" ;;
974
+ esac
975
+ }
976
+
977
+ INTENT=$(determine_intent)
978
+
979
+ # Record if we found symbols (from .purpose or commit message) and .paradigm/history exists
980
+ if [ -n "$SYMBOLS" ] && [ -d ".paradigm/history" ]; then
981
+ # Generate entry ID
982
+ if [ -f ".paradigm/history/log.jsonl" ]; then
983
+ COUNT=$(wc -l < ".paradigm/history/log.jsonl" | tr -d ' ')
984
+ COUNT=$((COUNT + 1))
985
+ else
986
+ COUNT=1
987
+ fi
988
+ ID=$(printf "h%04d" $COUNT)
989
+
990
+ # Create entry
991
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
992
+ AUTHOR=$(git config user.name || echo "unknown")
993
+
994
+ # Format symbols as JSON array
995
+ SYMBOLS_JSON=$(echo "$SYMBOLS" | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')
996
+
997
+ # Format files as JSON array
998
+ FILES_JSON=$(echo "$CHANGED_FILES" | tr '\\n' ',' | sed 's/,$//' | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')
999
+
1000
+ # Write entry
1001
+ echo "{\\"id\\":\\"$ID\\",\\"ts\\":\\"$TIMESTAMP\\",\\"type\\":\\"implement\\",\\"symbols\\":[$SYMBOLS_JSON],\\"author\\":{\\"type\\":\\"human\\",\\"id\\":\\"$AUTHOR\\"},\\"commit\\":\\"$COMMIT_HASH\\",\\"intent\\":\\"$INTENT\\",\\"files\\":[$FILES_JSON],\\"description\\":\\"$(echo "$COMMIT_MSG" | head -1 | sed 's/"/\\\\"/g')\\"}" >> .paradigm/history/log.jsonl
1002
+
1003
+ echo "[paradigm] History entry $ID recorded"
1004
+ fi
1005
+ `;
1006
+ var PRE_PUSH_HOOK = `#!/bin/sh
1007
+ # Paradigm pre-push hook - reindex history before pushing
1008
+ # Installed by: paradigm hooks install
1009
+
1010
+ if [ -d ".paradigm/history" ] && [ -f ".paradigm/history/log.jsonl" ]; then
1011
+ echo "[paradigm] Reindexing history..."
1012
+ npx paradigm history reindex 2>/dev/null || true
1013
+ fi
1014
+ `;
997
1015
  async function hooksInstallCommand(options = {}) {
998
1016
  const rootDir = process.cwd();
999
1017
  const onlyClaudeCode = options.claudeCode && !options.postCommit && !options.prePush && !options.cursor;
@@ -180,6 +180,47 @@ async function loadLoreEntry(rootDir, entryId) {
180
180
  const entries = await loadLoreEntries(rootDir);
181
181
  return entries.find((e) => e.id === entryId) || null;
182
182
  }
183
+ async function updateLoreEntry(rootDir, entryId, partial) {
184
+ const entry = await loadLoreEntry(rootDir, entryId);
185
+ if (!entry) return false;
186
+ const dateStr = entry.timestamp.slice(0, 10);
187
+ const entryPath = path.join(rootDir, LORE_DIR, ENTRIES_DIR, dateStr, `${entryId}.yaml`);
188
+ if (!fs.existsSync(entryPath)) return false;
189
+ if (partial.title !== void 0) entry.title = partial.title;
190
+ if (partial.summary !== void 0) entry.summary = partial.summary;
191
+ if (partial.type !== void 0) entry.type = partial.type;
192
+ if (partial.duration_minutes !== void 0) entry.duration_minutes = partial.duration_minutes;
193
+ if (partial.symbols_touched !== void 0) entry.symbols_touched = partial.symbols_touched;
194
+ if (partial.symbols_created !== void 0) entry.symbols_created = partial.symbols_created;
195
+ if (partial.files_created !== void 0) entry.files_created = partial.files_created;
196
+ if (partial.files_modified !== void 0) entry.files_modified = partial.files_modified;
197
+ if (partial.lines_added !== void 0) entry.lines_added = partial.lines_added;
198
+ if (partial.lines_removed !== void 0) entry.lines_removed = partial.lines_removed;
199
+ if (partial.commit !== void 0) entry.commit = partial.commit;
200
+ if (partial.decisions !== void 0) entry.decisions = partial.decisions;
201
+ if (partial.errors_encountered !== void 0) entry.errors_encountered = partial.errors_encountered;
202
+ if (partial.learnings !== void 0) entry.learnings = partial.learnings;
203
+ if (partial.verification !== void 0) entry.verification = partial.verification;
204
+ if (partial.tags !== void 0) entry.tags = partial.tags;
205
+ fs.writeFileSync(entryPath, yaml.dump(entry, { lineWidth: -1, noRefs: true }));
206
+ await rebuildTimeline(rootDir);
207
+ return true;
208
+ }
209
+ async function deleteLoreEntry(rootDir, entryId) {
210
+ const entry = await loadLoreEntry(rootDir, entryId);
211
+ if (!entry) return false;
212
+ const dateStr = entry.timestamp.slice(0, 10);
213
+ const entryPath = path.join(rootDir, LORE_DIR, ENTRIES_DIR, dateStr, `${entryId}.yaml`);
214
+ if (!fs.existsSync(entryPath)) return false;
215
+ fs.unlinkSync(entryPath);
216
+ const dateDir = path.dirname(entryPath);
217
+ const remaining = fs.readdirSync(dateDir).filter((f) => f.endsWith(".yaml"));
218
+ if (remaining.length === 0) {
219
+ fs.rmdirSync(dateDir);
220
+ }
221
+ await rebuildTimeline(rootDir);
222
+ return true;
223
+ }
183
224
  function migrateLegacyEntries(rootDir) {
184
225
  const entriesPath = path.join(rootDir, LORE_DIR, ENTRIES_DIR);
185
226
  if (!fs.existsSync(entriesPath)) return 0;
@@ -249,5 +290,7 @@ export {
249
290
  recordLore,
250
291
  loadLoreEntries,
251
292
  addReview,
252
- loadLoreEntry
293
+ loadLoreEntry,
294
+ updateLoreEntry,
295
+ deleteLoreEntry
253
296
  };
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ deleteLoreEntry,
4
+ loadLoreEntry
5
+ } from "./chunk-MVXJVRFI.js";
6
+ import "./chunk-MO4EEYFW.js";
7
+
8
+ // src/commands/lore/delete.ts
9
+ import chalk from "chalk";
10
+ async function loreDeleteCommand(id, options) {
11
+ const rootDir = process.cwd();
12
+ const entry = await loadLoreEntry(rootDir, id);
13
+ if (!entry) {
14
+ console.error(chalk.red(`
15
+ Entry not found: ${id}
16
+ `));
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ if (!options.yes) {
21
+ console.log(chalk.yellow(`
22
+ Will delete lore entry:`));
23
+ console.log(chalk.white(` ${entry.id} - ${entry.title}`));
24
+ console.log(chalk.gray(` Type: ${entry.type} | Author: ${entry.author.id} | ${entry.timestamp}`));
25
+ console.log(chalk.gray(` Symbols: ${entry.symbols_touched.join(", ")}`));
26
+ console.log(chalk.gray(`
27
+ Use --yes to confirm deletion.
28
+ `));
29
+ return;
30
+ }
31
+ const success = await deleteLoreEntry(rootDir, id);
32
+ if (success) {
33
+ console.log(chalk.green(`
34
+ Deleted lore entry: ${id}
35
+ `));
36
+ } else {
37
+ console.error(chalk.red(`
38
+ Failed to delete: ${id}
39
+ `));
40
+ process.exitCode = 1;
41
+ }
42
+ }
43
+ export {
44
+ loreDeleteCommand
45
+ };
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadLoreEntry,
4
+ updateLoreEntry
5
+ } from "./chunk-MVXJVRFI.js";
6
+ import "./chunk-MO4EEYFW.js";
7
+
8
+ // src/commands/lore/edit.ts
9
+ import chalk from "chalk";
10
+ async function loreEditCommand(id, options) {
11
+ const rootDir = process.cwd();
12
+ const entry = await loadLoreEntry(rootDir, id);
13
+ if (!entry) {
14
+ console.error(chalk.red(`
15
+ Entry not found: ${id}
16
+ `));
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+ const partial = {};
21
+ if (options.title) partial.title = options.title;
22
+ if (options.summary) partial.summary = options.summary;
23
+ if (options.type) {
24
+ const validTypes = ["agent-session", "human-note", "decision", "review", "incident", "milestone"];
25
+ if (!validTypes.includes(options.type)) {
26
+ console.error(chalk.red(`Invalid type: ${options.type}. Valid: ${validTypes.join(", ")}`));
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ partial.type = options.type;
31
+ }
32
+ if (options.symbols) {
33
+ partial.symbols_touched = options.symbols.split(",").map((s) => s.trim());
34
+ }
35
+ if (options.tags) {
36
+ partial.tags = options.tags.split(",").map((t) => t.trim());
37
+ }
38
+ if (options.learnings) {
39
+ partial.learnings = options.learnings.split(",").map((l) => l.trim());
40
+ }
41
+ if (Object.keys(partial).length === 0) {
42
+ console.log(chalk.yellow("\n No changes specified. Use --title, --summary, --type, --symbols, --tags, or --learnings.\n"));
43
+ return;
44
+ }
45
+ const success = await updateLoreEntry(rootDir, id, partial);
46
+ if (success) {
47
+ console.log(chalk.green(`
48
+ Updated lore entry: ${id}`));
49
+ for (const [key, value] of Object.entries(partial)) {
50
+ const display = Array.isArray(value) ? value.join(", ") : String(value);
51
+ console.log(chalk.gray(` ${key}: ${display}`));
52
+ }
53
+ console.log();
54
+ } else {
55
+ console.error(chalk.red(`
56
+ Failed to update: ${id}
57
+ `));
58
+ process.exitCode = 1;
59
+ }
60
+ }
61
+ export {
62
+ loreEditCommand
63
+ };