- Introduced new maps for navigating project knowledge, including "Current Work," "Fidelity Domain," "Fidelity Apps," "Work Items," and "People." - Created base files for daily notes, decisions, people, systems, work items, and workstreams with defined properties and views. - Developed templates for daily notes, decisions, meeting notes, persons, systems, work items, and workstreams to standardize documentation. - Updated scripts and prompts to reflect the new project-knowledge directory structure. - Removed outdated onboarding and start-here documents, consolidating relevant information into the new maps. - Ensured all references in workflows and scripts point to the new project-knowledge paths.
337 lines
8.8 KiB
Bash
Executable File
337 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
PROJECT_KNOWLEDGE_DIR="${AIW_PROJECT_KNOWLEDGE_DIR:-${AIW_MEMORY_VAULT_DIR:-${AIW_OBSIDIAN_VAULT_DIR:-$WORKSPACE_ROOT/project-knowledge}}}"
|
|
VAULT_DIR="$PROJECT_KNOWLEDGE_DIR"
|
|
BACKEND="${AIW_MEMORY_BACKEND:-auto}"
|
|
OBSIDIAN_CLI="$WORKSPACE_ROOT/scripts/obsidian/cli.sh"
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage: memory.sh <command> [args]
|
|
|
|
commands:
|
|
root
|
|
read <project-knowledge-relative-path>
|
|
search <query> [folder]
|
|
create <type> <slug> [title]
|
|
base-query <base-name> [format]
|
|
health
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
echo "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_vault() {
|
|
[[ -d "$PROJECT_KNOWLEDGE_DIR" ]] || die "project knowledge directory not found: $PROJECT_KNOWLEDGE_DIR"
|
|
}
|
|
|
|
has_obsidian() {
|
|
[[ "$BACKEND" != "files" ]] && command -v obsidian >/dev/null 2>&1 && [[ -x "$OBSIDIAN_CLI" ]]
|
|
}
|
|
|
|
require_obsidian_if_forced() {
|
|
if [[ "$BACKEND" == "obsidian" ]] && ! has_obsidian; then
|
|
die "AIW_MEMORY_BACKEND=obsidian but Obsidian CLI is unavailable"
|
|
fi
|
|
}
|
|
|
|
safe_path() {
|
|
local rel="$1"
|
|
[[ "$rel" != /* ]] || die "path must be project-knowledge-relative: $rel"
|
|
[[ "$rel" != *".."* ]] || die "path must not contain '..': $rel"
|
|
printf '%s/%s\n' "$VAULT_DIR" "$rel"
|
|
}
|
|
|
|
template_name_for_type() {
|
|
case "$1" in
|
|
daily) echo "daily" ;;
|
|
work-item) echo "work-item" ;;
|
|
person) echo "person" ;;
|
|
decision) echo "decision" ;;
|
|
system) echo "system" ;;
|
|
workstream) echo "workstream" ;;
|
|
meeting-note) echo "meeting-note" ;;
|
|
*) die "unsupported note type: $1" ;;
|
|
esac
|
|
}
|
|
|
|
folder_for_type() {
|
|
case "$1" in
|
|
daily) echo "06-daily" ;;
|
|
work-item) echo "02-work-items" ;;
|
|
person) echo "04-people" ;;
|
|
decision) echo "05-decisions" ;;
|
|
system) echo "03-context/systems" ;;
|
|
workstream) echo "03-context/workstreams" ;;
|
|
meeting-note) echo "06-daily" ;;
|
|
*) die "unsupported note type: $1" ;;
|
|
esac
|
|
}
|
|
|
|
note_path_for_type() {
|
|
local type="$1"
|
|
local slug="$2"
|
|
local folder
|
|
folder="$(folder_for_type "$type")"
|
|
case "$slug" in
|
|
*.md) echo "$folder/$slug" ;;
|
|
*) echo "$folder/$slug.md" ;;
|
|
esac
|
|
}
|
|
|
|
render_template() {
|
|
local template="$1"
|
|
local title="$2"
|
|
local slug="$3"
|
|
python3 - "$template" "$title" "$slug" <<'PY'
|
|
import datetime
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
template = pathlib.Path(sys.argv[1])
|
|
title = sys.argv[2]
|
|
slug = sys.argv[3]
|
|
now = datetime.datetime.now()
|
|
ticket = slug.removesuffix(".md").upper()
|
|
ticket = re.sub(r"^([A-Z]+)-?(\d+)$", r"\1-\2", ticket)
|
|
content = template.read_text()
|
|
content = content.replace("{{title}}", title)
|
|
content = content.replace("{{slug}}", slug.removesuffix(".md"))
|
|
content = content.replace("{{ticket}}", ticket)
|
|
content = content.replace("{{date}}", now.strftime("%Y-%m-%d"))
|
|
content = content.replace("{{date:YYYY-MM-DD}}", now.strftime("%Y-%m-%d"))
|
|
content = content.replace("{{time}}", now.strftime("%H:%M"))
|
|
print(content, end="")
|
|
PY
|
|
}
|
|
|
|
postprocess_note() {
|
|
local rel="$1"
|
|
local title="$2"
|
|
local slug="$3"
|
|
local target
|
|
target="$(safe_path "$rel")"
|
|
[[ -f "$target" ]] || return 0
|
|
python3 - "$target" "$title" "$slug" <<'PY'
|
|
import datetime
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
title = sys.argv[2]
|
|
slug = sys.argv[3].removesuffix(".md")
|
|
ticket = re.sub(r"^([A-Z]+)-?(\d+)$", r"\1-\2", slug.upper())
|
|
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
now = datetime.datetime.now().strftime("%H:%M")
|
|
|
|
content = path.read_text()
|
|
content = content.replace("{{title}}", title)
|
|
content = content.replace("{{slug}}", slug)
|
|
content = content.replace("{{ticket}}", ticket)
|
|
content = content.replace("{{date}}", today)
|
|
content = content.replace("{{date:YYYY-MM-DD}}", today)
|
|
content = content.replace("{{time}}", now)
|
|
|
|
lines = content.splitlines()
|
|
if lines[:1] == ["---"]:
|
|
try:
|
|
end = lines.index("---", 1)
|
|
except ValueError:
|
|
end = -1
|
|
if end > 0:
|
|
frontmatter = lines[1:end]
|
|
body = lines[end + 1 :]
|
|
for i, line in enumerate(frontmatter):
|
|
if line.startswith("title:"):
|
|
frontmatter[i] = f"title: {title}"
|
|
elif line.startswith("ticket:"):
|
|
frontmatter[i] = f"ticket: {ticket}"
|
|
elif line.startswith("date:"):
|
|
frontmatter[i] = f"date: {today}"
|
|
elif line.startswith("updated:"):
|
|
frontmatter[i] = f"updated: {today}"
|
|
lines = ["---", *frontmatter, "---", *body]
|
|
content = "\n".join(lines) + "\n"
|
|
|
|
path.write_text(content)
|
|
PY
|
|
}
|
|
|
|
create_direct() {
|
|
local type="$1"
|
|
local slug="$2"
|
|
local title="${3:-$slug}"
|
|
local rel target template_name template
|
|
rel="$(note_path_for_type "$type" "$slug")"
|
|
target="$(safe_path "$rel")"
|
|
template_name="$(template_name_for_type "$type")"
|
|
template="$VAULT_DIR/09-templates/$template_name.md"
|
|
|
|
[[ -e "$target" ]] && die "note already exists: $rel"
|
|
[[ -f "$template" ]] || die "template not found: 09-templates/$template_name.md"
|
|
mkdir -p "$(dirname "$target")"
|
|
render_template "$template" "$title" "$slug" > "$target"
|
|
postprocess_note "$rel" "$title" "$slug"
|
|
echo "$rel"
|
|
}
|
|
|
|
create_note() {
|
|
local type="$1"
|
|
local slug="$2"
|
|
local title="${3:-$slug}"
|
|
local rel template_name
|
|
rel="$(note_path_for_type "$type" "$slug")"
|
|
template_name="$(template_name_for_type "$type")"
|
|
|
|
require_obsidian_if_forced
|
|
if has_obsidian; then
|
|
if "$OBSIDIAN_CLI" create "path=$rel" "template=$template_name" >/dev/null 2>&1; then
|
|
postprocess_note "$rel" "$title" "$slug"
|
|
echo "$rel"
|
|
return 0
|
|
fi
|
|
[[ "$BACKEND" == "obsidian" ]] && die "obsidian create failed for $rel"
|
|
fi
|
|
|
|
create_direct "$type" "$slug" "$title"
|
|
}
|
|
|
|
search_memory() {
|
|
local query="$1"
|
|
local folder="${2:-}"
|
|
|
|
require_obsidian_if_forced
|
|
if has_obsidian; then
|
|
if [[ -n "$folder" ]]; then
|
|
if "$OBSIDIAN_CLI" search:context "query=$query" "path=$folder" "limit=50"; then
|
|
return 0
|
|
fi
|
|
else
|
|
if "$OBSIDIAN_CLI" search:context "query=$query" "limit=50"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
[[ "$BACKEND" == "obsidian" ]] && die "obsidian search failed"
|
|
fi
|
|
|
|
if [[ -n "$folder" ]]; then
|
|
rg -n -- "$query" "$VAULT_DIR/$folder"
|
|
else
|
|
rg -n -- "$query" "$VAULT_DIR"
|
|
fi
|
|
}
|
|
|
|
base_query() {
|
|
local base="$1"
|
|
local format="${2:-md}"
|
|
local base_path="08-bases/${base%.base}.base"
|
|
local type_name="${base%.base}"
|
|
|
|
require_obsidian_if_forced
|
|
if has_obsidian; then
|
|
if "$OBSIDIAN_CLI" base:query "path=$base_path" "format=$format"; then
|
|
return 0
|
|
fi
|
|
[[ "$BACKEND" == "obsidian" ]] && die "obsidian base query failed for $base_path"
|
|
fi
|
|
|
|
case "$type_name" in
|
|
work-items) rg -l '^type: work-item$' "$VAULT_DIR/02-work-items" ;;
|
|
people) rg -l '^type: person$' "$VAULT_DIR/04-people" ;;
|
|
decisions) rg -l '^type: decision$' "$VAULT_DIR/05-decisions" ;;
|
|
daily) rg -l '^type: daily$' "$VAULT_DIR/06-daily" ;;
|
|
systems) rg -l '^type: system$' "$VAULT_DIR/03-context/systems" ;;
|
|
workstreams) rg -l '^type: workstream$' "$VAULT_DIR/03-context/workstreams" ;;
|
|
*) die "unsupported base fallback: $base" ;;
|
|
esac
|
|
}
|
|
|
|
health_check() {
|
|
local failed=0
|
|
local required=(
|
|
"00-start/start-here.md"
|
|
"01-current/current-work.md"
|
|
"01-current/work-items.md"
|
|
"02-work-items/index.md"
|
|
"03-context/project.md"
|
|
"04-people/index.md"
|
|
"07-maps/index.md"
|
|
"08-bases/work-items.base"
|
|
"09-templates/work-item.md"
|
|
)
|
|
|
|
ensure_vault
|
|
|
|
for rel in "${required[@]}"; do
|
|
if [[ ! -e "$VAULT_DIR/$rel" ]]; then
|
|
echo "missing: $rel"
|
|
failed=1
|
|
fi
|
|
done
|
|
|
|
if [[ -d "$WORKSPACE_ROOT/.obsidian" ]]; then
|
|
echo "unexpected root Obsidian config: .obsidian"
|
|
failed=1
|
|
fi
|
|
|
|
if has_obsidian; then
|
|
"$OBSIDIAN_CLI" unresolved total 2>/dev/null | sed 's/^/unresolved-links: /' || true
|
|
"$OBSIDIAN_CLI" orphans total 2>/dev/null | sed 's/^/orphans: /' || true
|
|
"$OBSIDIAN_CLI" tags total 2>/dev/null | sed 's/^/tags: /' || true
|
|
else
|
|
echo "obsidian-cli: unavailable; file checks only"
|
|
fi
|
|
|
|
if [[ "$failed" -eq 0 ]]; then
|
|
echo "memory health ok"
|
|
fi
|
|
exit "$failed"
|
|
}
|
|
|
|
main() {
|
|
ensure_vault
|
|
local command="${1:-}"
|
|
[[ -n "$command" ]] || { usage; exit 1; }
|
|
shift || true
|
|
|
|
case "$command" in
|
|
root)
|
|
echo "$VAULT_DIR"
|
|
;;
|
|
read)
|
|
[[ $# -eq 1 ]] || die "usage: memory.sh read <project-knowledge-relative-path>"
|
|
cat "$(safe_path "$1")"
|
|
;;
|
|
search)
|
|
[[ $# -ge 1 ]] || die "usage: memory.sh search <query> [folder]"
|
|
search_memory "$1" "${2:-}"
|
|
;;
|
|
create)
|
|
[[ $# -ge 2 ]] || die "usage: memory.sh create <type> <slug> [title]"
|
|
create_note "$1" "$2" "${3:-$2}"
|
|
;;
|
|
base-query)
|
|
[[ $# -ge 1 ]] || die "usage: memory.sh base-query <base-name> [format]"
|
|
base_query "$1" "${2:-md}"
|
|
;;
|
|
health)
|
|
health_check
|
|
;;
|
|
*)
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|