project-logbook 0.3.3 → 0.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 (75) hide show
  1. package/dist/commands/build.js +54 -76
  2. package/dist/commands/init.js +11 -11
  3. package/dist/commands/lint.js +27 -10
  4. package/dist/commands/list.js +18 -18
  5. package/dist/commands/log.js +5 -5
  6. package/dist/commands/new.js +19 -20
  7. package/dist/commands/preview.js +9 -8
  8. package/dist/commands/release.js +6 -6
  9. package/dist/commands/start.js +26 -21
  10. package/dist/commands/status.d.ts +1 -0
  11. package/dist/commands/status.js +138 -0
  12. package/dist/commands/steer.js +9 -9
  13. package/dist/commands/upgrade.js +5 -5
  14. package/dist/index.js +9 -8
  15. package/dist/lib/build-helpers.js +30 -67
  16. package/dist/lib/build-steps.d.ts +20 -0
  17. package/dist/lib/build-steps.js +57 -0
  18. package/dist/lib/config.d.ts +17 -0
  19. package/dist/lib/config.js +27 -3
  20. package/dist/lib/entry-id.d.ts +22 -0
  21. package/dist/lib/entry-id.js +26 -0
  22. package/dist/lib/entry-paths.d.ts +23 -0
  23. package/dist/lib/entry-paths.js +55 -0
  24. package/dist/lib/git-helpers.d.ts +32 -1
  25. package/dist/lib/git-helpers.js +119 -26
  26. package/dist/lib/hast-helpers.d.ts +10 -0
  27. package/dist/lib/hast-helpers.js +22 -0
  28. package/dist/lib/html-attributes.d.ts +17 -0
  29. package/dist/lib/html-attributes.js +17 -0
  30. package/dist/lib/html-escape.d.ts +16 -0
  31. package/dist/lib/html-escape.js +38 -0
  32. package/dist/lib/image-helpers.js +26 -33
  33. package/dist/lib/lint-runner.js +5 -5
  34. package/dist/lib/markdown-processors.d.ts +22 -0
  35. package/dist/lib/markdown-processors.js +68 -0
  36. package/dist/lib/package-version.d.ts +5 -0
  37. package/dist/lib/package-version.js +16 -0
  38. package/dist/lib/rss.d.ts +29 -0
  39. package/dist/lib/rss.js +77 -0
  40. package/dist/lib/styles.js +5 -2
  41. package/dist/lib/template-helpers.d.ts +4 -3
  42. package/dist/lib/template-helpers.js +51 -27
  43. package/dist/lib/template-types.d.ts +2 -2
  44. package/dist/lib/templates.d.ts +12 -3
  45. package/dist/lib/templates.js +59 -52
  46. package/dist/lib/theme.d.ts +37 -0
  47. package/dist/lib/theme.js +50 -0
  48. package/dist/lib/url-helpers.d.ts +13 -0
  49. package/dist/lib/url-helpers.js +27 -0
  50. package/dist/linters/diff-to-narrative.d.ts +6 -0
  51. package/dist/linters/diff-to-narrative.js +114 -0
  52. package/dist/linters/index.js +4 -0
  53. package/dist/linters/technical-log.d.ts +7 -0
  54. package/dist/linters/technical-log.js +72 -0
  55. package/dist/templates/CONTRIBUTING.md +12 -3
  56. package/dist/templates/index.md +10 -6
  57. package/dist/templates/log.md +5 -0
  58. package/dist/templates/logbook-client.js +42 -16
  59. package/dist/templates/steer.txt +21 -5
  60. package/dist/templates/styles.css +121 -0
  61. package/dist/utils/date.d.ts +30 -1
  62. package/dist/utils/date.js +68 -15
  63. package/dist/utils/frontmatter.d.ts +26 -0
  64. package/dist/utils/frontmatter.js +37 -0
  65. package/dist/utils/fs.d.ts +13 -0
  66. package/dist/utils/fs.js +23 -0
  67. package/dist/utils/log-timeline.d.ts +69 -0
  68. package/dist/utils/log-timeline.js +218 -0
  69. package/package.json +4 -2
  70. package/src/templates/CONTRIBUTING.md +12 -3
  71. package/src/templates/index.md +10 -6
  72. package/src/templates/log.md +5 -0
  73. package/src/templates/logbook-client.js +42 -16
  74. package/src/templates/steer.txt +21 -5
  75. package/src/templates/styles.css +121 -0
@@ -0,0 +1,37 @@
1
+ /** Success message styling (green) */
2
+ export declare const success: import("chalk").ChalkInstance;
3
+ /** Error message styling (red) */
4
+ export declare const error: import("chalk").ChalkInstance;
5
+ /** Warning message styling (yellow) */
6
+ export declare const warning: import("chalk").ChalkInstance;
7
+ /** Info/secondary message styling (blue) */
8
+ export declare const info: import("chalk").ChalkInstance;
9
+ /** Neutral/disabled styling (gray) */
10
+ export declare const neutral: import("chalk").ChalkInstance;
11
+ /** Highlight/emphasis styling (cyan) */
12
+ export declare const highlight: import("chalk").ChalkInstance;
13
+ /** Bold text */
14
+ export declare const bold: import("chalk").ChalkInstance;
15
+ /**
16
+ * Format a section header (e.g., "Checking project integrity...").
17
+ * Uses blue color for visual separation.
18
+ */
19
+ export declare function header(text: string): string;
20
+ /**
21
+ * Format an error message with context.
22
+ * @param prefix - The category/command name
23
+ * @param message - The error message
24
+ */
25
+ export declare function errorMessage(prefix: string, message: string): string;
26
+ /**
27
+ * Format a warning message with context.
28
+ * @param prefix - The category/command name
29
+ * @param message - The warning message
30
+ */
31
+ export declare function warningMessage(prefix: string, message: string): string;
32
+ /**
33
+ * Format a success message with context.
34
+ * @param prefix - The category/command name
35
+ * @param message - The success message
36
+ */
37
+ export declare function successMessage(prefix: string, message: string): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Centralized styling/theme for CLI output.
3
+ * Ensures consistent colors and visual hierarchy across all commands.
4
+ */
5
+ import chalk from 'chalk';
6
+ /** Success message styling (green) */
7
+ export const success = chalk.green;
8
+ /** Error message styling (red) */
9
+ export const error = chalk.red;
10
+ /** Warning message styling (yellow) */
11
+ export const warning = chalk.yellow;
12
+ /** Info/secondary message styling (blue) */
13
+ export const info = chalk.blue;
14
+ /** Neutral/disabled styling (gray) */
15
+ export const neutral = chalk.gray;
16
+ /** Highlight/emphasis styling (cyan) */
17
+ export const highlight = chalk.cyan;
18
+ /** Bold text */
19
+ export const bold = chalk.bold;
20
+ /**
21
+ * Format a section header (e.g., "Checking project integrity...").
22
+ * Uses blue color for visual separation.
23
+ */
24
+ export function header(text) {
25
+ return info(text);
26
+ }
27
+ /**
28
+ * Format an error message with context.
29
+ * @param prefix - The category/command name
30
+ * @param message - The error message
31
+ */
32
+ export function errorMessage(prefix, message) {
33
+ return `${error(prefix)}: ${message}`;
34
+ }
35
+ /**
36
+ * Format a warning message with context.
37
+ * @param prefix - The category/command name
38
+ * @param message - The warning message
39
+ */
40
+ export function warningMessage(prefix, message) {
41
+ return `${warning(prefix)}: ${message}`;
42
+ }
43
+ /**
44
+ * Format a success message with context.
45
+ * @param prefix - The category/command name
46
+ * @param message - The success message
47
+ */
48
+ export function successMessage(prefix, message) {
49
+ return `${success(prefix)}: ${message}`;
50
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check if a URL/path is external (has an absolute URI scheme).
3
+ * @param url - The URL or path to check
4
+ * @returns true if the URL has an absolute URI scheme (http://, https://, ftp://, etc.)
5
+ */
6
+ export declare function isExternalUrl(url: string): boolean;
7
+ /**
8
+ * Check if a value is an external URL or an anchor link.
9
+ * Used to identify URLs that should not be processed (rewritten, collected, etc.)
10
+ * @param href - The href/src value to check
11
+ * @returns true if the value is external or an anchor
12
+ */
13
+ export declare function isExternalUrlOrAnchor(href: string): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Regex pattern to detect absolute URLs with URI schemes.
3
+ * Matches URIs like: http://, https://, ftp://, data:, etc.
4
+ * Pattern breakdown:
5
+ * ^[a-z] - Starts with a letter (lowercase)
6
+ * [a-z\d+\-.]*: - Followed by alphanumeric, +, -, or . characters, ending with :
7
+ * Example matches: http://, https://, ftp://, mailto:, data:
8
+ * Used to distinguish external URLs from relative paths.
9
+ */
10
+ const EXTERNAL_URL_SCHEME_REGEX = /^[a-z][a-z\d+\-.]*:/i;
11
+ /**
12
+ * Check if a URL/path is external (has an absolute URI scheme).
13
+ * @param url - The URL or path to check
14
+ * @returns true if the URL has an absolute URI scheme (http://, https://, ftp://, etc.)
15
+ */
16
+ export function isExternalUrl(url) {
17
+ return EXTERNAL_URL_SCHEME_REGEX.test(url);
18
+ }
19
+ /**
20
+ * Check if a value is an external URL or an anchor link.
21
+ * Used to identify URLs that should not be processed (rewritten, collected, etc.)
22
+ * @param href - The href/src value to check
23
+ * @returns true if the value is external or an anchor
24
+ */
25
+ export function isExternalUrlOrAnchor(href) {
26
+ return EXTERNAL_URL_SCHEME_REGEX.test(href) || href.startsWith('#');
27
+ }
@@ -0,0 +1,6 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ export declare function stripFrontmatter(content: string): string;
3
+ export declare function stripMarkdown(text: string): string;
4
+ export declare function getWordCount(content: string): number;
5
+ declare const linter: Linter;
6
+ export default linter;
@@ -0,0 +1,114 @@
1
+ import { getActiveEntry } from '../lib/session.js';
2
+ import { getChangedLOC } from '../lib/git-helpers.js';
3
+ import fs from 'fs-extra';
4
+ import { join } from 'node:path';
5
+ // Thresholds & Limits Configuration
6
+ const TRIVIAL_LOC_MAX = 20;
7
+ const STANDARD_LOC_MAX = 150;
8
+ const TRIVIAL_WORDS = { min: 30, max: 150 };
9
+ const STANDARD_WORDS = { min: 50, max: 300 };
10
+ const LARGE_WORDS = { min: 100, max: 400 };
11
+ export function stripFrontmatter(content) {
12
+ const lines = content.split(/\r?\n/);
13
+ if (lines[0]?.trim() === '---') {
14
+ const endIdx = lines.indexOf('---', 1);
15
+ if (endIdx !== -1) {
16
+ return lines.slice(endIdx + 1).join('\n');
17
+ }
18
+ }
19
+ return content;
20
+ }
21
+ export function stripMarkdown(text) {
22
+ return text
23
+ .replace(/<[^>]*>/g, '') // HTML tags
24
+ .replace(/!\[([^\]]*)]\([^)]*\)/g, '$1') // images
25
+ .replace(/\[([^\]]*)]\([^)]*\)/g, '$1') // links
26
+ .replace(/```[\s\S]*?```/g, '') // code blocks
27
+ .replace(/`([^`]+)`/g, '$1') // inline code
28
+ .replace(/(\*\*|__)(.*?)\1/g, '$2') // bold
29
+ .replace(/(\*|_)(.*?)\1/g, '$2') // italic
30
+ .replace(/^\s*>\s+/gm, '') // blockquotes
31
+ .replace(/^\s*#+\s+/gm, '') // headings
32
+ .replace(/^\s*[-*+]\s+/gm, '') // list bullets
33
+ .replace(/^\s*\d+\.\s+/gm, ''); // ordered lists
34
+ }
35
+ export function getWordCount(content) {
36
+ const clean = stripMarkdown(stripFrontmatter(content));
37
+ return clean.split(/\s+/).filter((w) => w.trim().length > 0).length;
38
+ }
39
+ const linter = {
40
+ name: 'diff-to-narrative',
41
+ description: 'Checks word count of active index.md against total LOC changed',
42
+ async check(context) {
43
+ // Only target project-level checks
44
+ if (context.entryName)
45
+ return [];
46
+ const active = await getActiveEntry();
47
+ if (!active || active.source !== 'lockfile') {
48
+ return [];
49
+ }
50
+ const logbookDir = context.config.logbookDir;
51
+ const { total: totalLOC } = await getChangedLOC(logbookDir);
52
+ // If there are no active changes and the tree is completely clean, skip gracefully
53
+ if (totalLOC === 0) {
54
+ return [];
55
+ }
56
+ const indexPath = join(process.cwd(), logbookDir, active.slug, 'index.md');
57
+ if (!(await fs.pathExists(indexPath))) {
58
+ return [];
59
+ }
60
+ const content = await fs.readFile(indexPath, 'utf-8');
61
+ const wordCount = getWordCount(content);
62
+ const issues = [];
63
+ if (totalLOC < TRIVIAL_LOC_MAX) {
64
+ if (wordCount < TRIVIAL_WORDS.min) {
65
+ issues.push({
66
+ level: 'warning',
67
+ category: 'story-ratio',
68
+ message: `Code change is trivial (${totalLOC} LOC), but index.md has only ${wordCount} words (minimum is ${TRIVIAL_WORDS.min}). Please expand the narrative.`,
69
+ });
70
+ }
71
+ else if (wordCount > TRIVIAL_WORDS.max) {
72
+ issues.push({
73
+ level: 'warning',
74
+ category: 'story-ratio',
75
+ message: `Warning: Code change is trivial, but index.md exceeds ${TRIVIAL_WORDS.max} words. Please summarize.`,
76
+ });
77
+ }
78
+ }
79
+ else if (totalLOC <= STANDARD_LOC_MAX) {
80
+ if (wordCount < STANDARD_WORDS.min) {
81
+ issues.push({
82
+ level: 'warning',
83
+ category: 'story-ratio',
84
+ message: `Code change is standard (${totalLOC} LOC), but index.md has only ${wordCount} words (minimum is ${STANDARD_WORDS.min}). Please expand the narrative.`,
85
+ });
86
+ }
87
+ else if (wordCount > STANDARD_WORDS.max) {
88
+ issues.push({
89
+ level: 'warning',
90
+ category: 'story-ratio',
91
+ message: `Warning: Code change is standard (${totalLOC} LOC), but index.md exceeds ${STANDARD_WORDS.max} words. Please summarize.`,
92
+ });
93
+ }
94
+ }
95
+ else {
96
+ if (wordCount < LARGE_WORDS.min) {
97
+ issues.push({
98
+ level: 'warning',
99
+ category: 'story-ratio',
100
+ message: `Code change is large (${totalLOC} LOC), but index.md has only ${wordCount} words (minimum is ${LARGE_WORDS.min}). Please expand the narrative.`,
101
+ });
102
+ }
103
+ else if (wordCount > LARGE_WORDS.max) {
104
+ issues.push({
105
+ level: 'warning',
106
+ category: 'story-ratio',
107
+ message: `Warning: Story exceeds ${LARGE_WORDS.max} words. Consider summarizing and linking to a Wiki/ADR.`,
108
+ });
109
+ }
110
+ }
111
+ return issues;
112
+ },
113
+ };
114
+ export default linter;
@@ -1,3 +1,4 @@
1
+ import diffToNarrative from './diff-to-narrative.js';
1
2
  import frontmatter from './frontmatter.js';
2
3
  import jiraPrefix from './jira-prefix.js';
3
4
  import links from './links.js';
@@ -5,8 +6,10 @@ import lockfile from './lockfile.js';
5
6
  import placeholders from './placeholders.js';
6
7
  import projectIntegrity from './project-integrity.js';
7
8
  import readability from './readability.js';
9
+ import technicalLog from './technical-log.js';
8
10
  import workspaces from './workspaces.js';
9
11
  export const linters = [
12
+ diffToNarrative,
10
13
  frontmatter,
11
14
  jiraPrefix,
12
15
  links,
@@ -14,5 +17,6 @@ export const linters = [
14
17
  placeholders,
15
18
  projectIntegrity,
16
19
  readability,
20
+ technicalLog,
17
21
  workspaces,
18
22
  ];
@@ -0,0 +1,7 @@
1
+ import type { Linter } from '../lib/lint-types.js';
2
+ /**
3
+ * Linter that checks for adequate technical logging.
4
+ * Ensures log.md has more than just the initial "Started investigation" entry.
5
+ */
6
+ declare const technicalLog: Linter;
7
+ export default technicalLog;
@@ -0,0 +1,72 @@
1
+ import fs from 'fs-extra';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Linter that checks for adequate technical logging.
5
+ * Ensures log.md has more than just the initial "Started investigation" entry.
6
+ */
7
+ const technicalLog = {
8
+ name: 'technical-log',
9
+ description: 'Checks for adequate technical logging in log.md',
10
+ async check(context) {
11
+ const issues = [];
12
+ // Only check the active entry (the one with the lockfile)
13
+ // Old entries cannot be updated, so we don't warn about them
14
+ if (!context.entryName || !context.entryPath) {
15
+ return issues;
16
+ }
17
+ // Check if this is the active entry by verifying lockfile exists
18
+ const lockfilePath = join(process.cwd(), '.logbook-active');
19
+ if (!(await fs.pathExists(lockfilePath))) {
20
+ // No active entry, skip this linter
21
+ return issues;
22
+ }
23
+ const lockfileContent = await fs.readFile(lockfilePath, 'utf8');
24
+ const lockfileData = JSON.parse(lockfileContent);
25
+ const activeEntrySlug = lockfileData.slug;
26
+ // Only check if this entry matches the active entry slug
27
+ if (context.entryName !== activeEntrySlug) {
28
+ return issues;
29
+ }
30
+ const logPath = join(context.entryPath, 'log.md');
31
+ if (!(await fs.pathExists(logPath))) {
32
+ issues.push({
33
+ category: 'TECHNICAL_LOG',
34
+ level: 'warning',
35
+ message: 'Missing log.md file. Create one to document your technical decisions.',
36
+ });
37
+ return issues;
38
+ }
39
+ const logContent = await fs.readFile(logPath, 'utf8');
40
+ // Extract all protocol entries (lines starting with "- " under ## Protocol)
41
+ const protocolSection = logContent.split('## Protocol')[1];
42
+ if (!protocolSection) {
43
+ // No Protocol section found, might be malformed
44
+ issues.push({
45
+ category: 'TECHNICAL_LOG',
46
+ level: 'warning',
47
+ message: 'log.md missing "## Protocol" section. Add one to structure your log entries.',
48
+ });
49
+ return issues;
50
+ }
51
+ const lines = protocolSection.split('\n').filter((line) => line.trim().startsWith('- '));
52
+ const entryCount = lines.length;
53
+ // Check if there's only the initial "Started investigation" entry
54
+ if (entryCount <= 1) {
55
+ issues.push({
56
+ category: 'TECHNICAL_LOG',
57
+ level: 'warning',
58
+ message: 'log.md only contains the initial "Started investigation" entry. Log your work in real-time using `logbook log "<message>"` after each significant step.',
59
+ });
60
+ }
61
+ else if (entryCount < 5) {
62
+ // Less than 5 entries might indicate insufficient logging
63
+ issues.push({
64
+ category: 'TECHNICAL_LOG',
65
+ level: 'warning',
66
+ message: `log.md has only ${entryCount} entries. Consider adding more detail about your implementation process, decisions, and challenges.`,
67
+ });
68
+ }
69
+ return issues;
70
+ },
71
+ };
72
+ export default technicalLog;
@@ -47,12 +47,21 @@ Only update the currently active logbook entry. Do not edit other existing entri
47
47
  When you finish writing `index.md`, **remove the boilerplate link line** that the template inserts:
48
48
 
49
49
  ### 3. Quality Assurance
50
- Before submitting your work, you MUST release the active entry and run the linter:
50
+ Before finalizing your work, you should run the linter to verify there are no errors:
51
51
  ```bash
52
- logbook release
53
52
  logbook lint
54
53
  ```
55
- `logbook release` removes the `.logbook-active` lockfile. The linter will fail if the lockfile is still present, enforcing that no entry is left dangling in a committed state.
54
+
55
+ You can also check the overall status of the logbook, configuration settings, and statistics (total tasks, done, and drafts) using:
56
+ ```bash
57
+ logbook status
58
+ ```
59
+
60
+ The linter will warn you if the lockfile `.logbook-active` is still present, which is expected during active development.
61
+
62
+ Once the work is ready:
63
+ - **If you are an AI agent**: Do not release the entry. Hand back to the prompter, who will review and run `logbook release`.
64
+ - **If you are a human developer**: Run `logbook release` to remove the lockfile and finalize the entry.
56
65
 
57
66
  The linter enforces rules to be followed.
58
67
 
@@ -3,24 +3,28 @@
3
3
  ticket: {{id}}
4
4
  # The title should be a human-readable description of the work
5
5
  title: {{title}}
6
- prompter: [PROMPTER]
7
- harness: [HARNESS]
8
- llm: [LLM]
9
- summary: [WRITE_SUMMARY_HERE]
6
+ prompter: "[PROMPTER]"
7
+ harness: "[HARNESS]"
8
+ llm: "[LLM]"
9
+ summary: "[WRITE_SUMMARY_HERE]"
10
10
  # Tags for categorizing the change (must be from allowed list in .project-logbook)
11
11
  tags: []
12
12
  # Workspace(s) this change affects (must match package.json workspaces; leave empty for single-project)
13
13
  # Example: workspaces: ["frontend", "shared"]
14
14
  workspaces: []
15
15
  # Set automatically by `logbook start`
16
- dateStart: [DATE_START]
16
+ dateStart: "[DATE_START]"
17
17
  # Set automatically by `logbook release`
18
- dateEnd: [DATE_END]
18
+ dateEnd: "[DATE_END]"
19
19
  ---
20
20
 
21
21
  ## Summary
22
22
  TODO: Write a polished, highly readable ticket summary that reads like an engaging technical narrative (similar to a well-written dev blog post).
23
23
 
24
+ ### Before You Start:
25
+ - **Check `log.md`**: Review your technical log for all the decisions, errors, and pivots you recorded during implementation.
26
+ - **Reference the log**: Use your real-time log entries as source material for the narrative — don't try to reconstruct from memory.
27
+
24
28
  ### Formatting & Style Rules:
25
29
  - **Maintain the narrative tone:** TODO: Keep the storytelling flair (e.g., describing how problems accumulated or how gaps surfaced), but stay strictly factual based on the provided changes.
26
30
  - **Add thematic headings:** TODO: Break the narrative down into logical chapters using Markdown headings (e.g., ### The Friction Points, ### The Fix, ### Closing the Gap).
@@ -1,4 +1,9 @@
1
1
  # Technical Log: {{id}}-{{slug}}
2
2
 
3
+ > **REMINDER**: Log your work in real-time using `logbook log "<message>"`. Don't wait until the end!
4
+ >
5
+ > Log after every significant step: investigation, errors, decisions, code changes, test runs, etc.
6
+ >
7
+
3
8
  ## Protocol
4
9
  - {{fullIso}}: Started investigation.
@@ -2,27 +2,40 @@
2
2
  'use strict';
3
3
 
4
4
  /* ── 1. Real-time relative dates ──────────────────────────────────────── */
5
- function relativeDate(isoString) {
6
- var d = new Date(isoString);
5
+ /**
6
+ * Calculate the relative date string (today, yesterday, 2 days ago, etc)
7
+ * Prepend the relative component to the locally-formatted absolute date.
8
+ */
9
+ function getRelativeComponent(d) {
7
10
  var now = new Date();
8
11
  // Normalise both to midnight local time for day-diff calculation
9
12
  var dDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
10
13
  var nDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
11
14
  var diffDays = Math.round((nDay - dDay) / 86400000);
12
15
 
13
- var relative;
14
- if (diffDays === 0) relative = 'today';
15
- else if (diffDays === 1) relative = 'yesterday';
16
- else if (diffDays < 7) relative = diffDays + ' days ago';
17
- else if (diffDays < 14) relative = '1 week ago';
18
- else if (diffDays < 30) relative = Math.floor(diffDays / 7) + ' weeks ago';
19
- else if (diffDays < 60) relative = '1 month ago';
20
- else if (diffDays < 365) relative = Math.floor(diffDays / 30) + ' months ago';
21
- else if (diffDays < 730) relative = '1 year ago';
22
- else relative = Math.floor(diffDays / 365) + ' years ago';
23
-
24
- var absolute = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
25
- return relative + ', ' + absolute;
16
+ if (diffDays === 0) return 'today';
17
+ else if (diffDays === 1) return 'yesterday';
18
+ else if (diffDays < 7) return diffDays + ' days ago';
19
+ else if (diffDays < 14) return '1 week ago';
20
+ else if (diffDays < 30) return Math.floor(diffDays / 7) + ' weeks ago';
21
+ else if (diffDays < 60) return '1 month ago';
22
+ else if (diffDays < 365) return Math.floor(diffDays / 30) + ' months ago';
23
+ else if (diffDays < 730) return '1 year ago';
24
+ else return Math.floor(diffDays / 365) + ' years ago';
25
+ }
26
+
27
+ /**
28
+ * Format a Date object as absolute date and time in browser's local timezone.
29
+ * Format: "26 May 2026, 10:51"
30
+ */
31
+ function formatAbsoluteComponent(d) {
32
+ var day = d.getDate();
33
+ var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
34
+ var month = months[d.getMonth()];
35
+ var year = d.getFullYear();
36
+ var hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours();
37
+ var minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes();
38
+ return day + ' ' + month + ' ' + year + ', ' + hours + ':' + minutes;
26
39
  }
27
40
 
28
41
  function updateRelativeDates() {
@@ -30,7 +43,20 @@
30
43
  for (var i = 0; i < els.length; i++) {
31
44
  var el = els[i];
32
45
  var iso = el.getAttribute('data-date');
33
- if (iso) el.textContent = relativeDate(iso);
46
+ if (iso) {
47
+ var d = new Date(iso);
48
+ if (!isNaN(d.getTime())) {
49
+ // Format the absolute date in the user's browser/local timezone
50
+ var absolute = formatAbsoluteComponent(d);
51
+ // Prepend the relative component (e.g., "today")
52
+ var relative = getRelativeComponent(d);
53
+ if (relative) {
54
+ el.textContent = relative + ', ' + absolute;
55
+ } else {
56
+ el.textContent = absolute;
57
+ }
58
+ }
59
+ }
34
60
  }
35
61
  }
36
62
 
@@ -8,11 +8,27 @@ Phase 1: Understand
8
8
  2. Review `AGENTS.md` and `CONTRIBUTING.md` if you need architectural or workflow context.
9
9
 
10
10
  Phase 2: Execute & Trace
11
- 1. Use `log.md` as a live technical scratchpad.
12
- 2. Log your work in real-time using the `logbook log "<message>"` command. Record every major decision, error, and pivot. Do not reconstruct this at the end.
13
- Example: logbook log "Investigated root cause found issue in src/lib/config.ts"
14
- One message per call. For multiple entries, call the command once per message.
15
- 3. Stick to the active entry; never modify past entries in the logbook folder.
11
+
12
+ **CRITICAL: Real-Time Logging**
13
+ You MUST log your work continuously using `logbook log "<message>"`. This is not optional.
14
+
15
+ 1. Use `log.md` as your live technical scratchpad.
16
+ 2. After EVERY significant step, run: `logbook log "<what you just did>"`
17
+ - Investigated a file? Log it.
18
+ - Found an error? Log it.
19
+ - Made a design decision? Log it.
20
+ - Fixed a bug? Log it.
21
+ - Ran tests? Log the result.
22
+
23
+ Examples:
24
+ - `logbook log "Investigated src/lib/config.ts - found missing validation"`
25
+ - `logbook log "Created new RSS module with generateRssFeed() function"`
26
+ - `logbook log "Test failed: TypeScript error on line 42 - fixed type assertion"`
27
+ - `logbook log "All 127 tests pass, pre-commit suite successful"`
28
+
29
+ 3. **One message per command call.** For multiple entries, call the command multiple times.
30
+ 4. Do NOT reconstruct the log at the end. If the log is empty when you finish, you did it wrong.
31
+ 5. Stick to the active entry; never modify past entries in the logbook folder.
16
32
 
17
33
  Phase 3: Synthesize
18
34
  1. When implementation is finished, write the narrative in `index.md`.
@@ -33,6 +33,11 @@ header h1 {
33
33
  header h1 a {
34
34
  text-decoration: none !important;
35
35
  color: black !important;
36
+ transition: color 0.2s;
37
+ }
38
+
39
+ header h1 a:hover {
40
+ color: var(--primary) !important;
36
41
  }
37
42
 
38
43
  .tagline {
@@ -691,3 +696,119 @@ article img[src$='.webp'],
691
696
  article img[src$='.svg'] {
692
697
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
693
698
  }
699
+
700
+ /* Technical Log Timeline */
701
+ #log .timeline {
702
+ list-style: none;
703
+ padding: 0;
704
+ margin: 0;
705
+ position: relative;
706
+ }
707
+
708
+ #log .timeline::before {
709
+ content: '';
710
+ position: absolute;
711
+ left: 8px;
712
+ top: 0;
713
+ bottom: 0;
714
+ width: 2px;
715
+ background: var(--border);
716
+ }
717
+
718
+ #log .timeline-entry {
719
+ position: relative;
720
+ padding-left: 2rem;
721
+ margin-bottom: 0.75rem;
722
+ min-height: 1.5rem;
723
+ display: grid;
724
+ grid-template-columns: 55px 1fr;
725
+ gap: 0.5rem;
726
+ }
727
+
728
+ #log .timeline-entry.no-time .timeline-message {
729
+ grid-column: 2;
730
+ }
731
+
732
+ #log .timeline-entry.has-time::before {
733
+ content: '';
734
+ position: absolute;
735
+ left: 5px;
736
+ top: 4px;
737
+ width: 8px;
738
+ height: 8px;
739
+ border-radius: 50%;
740
+ background: var(--primary);
741
+ border: 2px solid var(--bg);
742
+ z-index: 1;
743
+ }
744
+
745
+ #log .timeline-entry.no-time::before {
746
+ content: '';
747
+ position: absolute;
748
+ left: 7px;
749
+ top: 0.6rem;
750
+ width: 4px;
751
+ height: 4px;
752
+ border-radius: 50%;
753
+ background: var(--text-muted);
754
+ opacity: 0.6;
755
+ }
756
+
757
+ #log .timeline-time {
758
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
759
+ font-size: 0.75rem;
760
+ font-weight: 600;
761
+ color: var(--primary);
762
+ display: inline-block;
763
+ }
764
+
765
+ #log .timeline-message {
766
+ font-size: 0.9375rem;
767
+ color: var(--text);
768
+ line-height: 1.5;
769
+ }
770
+
771
+ #log .timeline-entry.no-time .timeline-message {
772
+ color: var(--text);
773
+ opacity: 0.85;
774
+ }
775
+
776
+ #log .timeline-gap {
777
+ position: relative;
778
+ padding-left: 2rem;
779
+ margin: 1.5rem 0;
780
+ min-height: 1rem;
781
+ }
782
+
783
+ #log .timeline-gap::before {
784
+ content: '';
785
+ position: absolute;
786
+ left: 7px;
787
+ top: 0.4rem;
788
+ width: 4px;
789
+ height: 4px;
790
+ border-radius: 50%;
791
+ background: var(--text-muted);
792
+ opacity: 0.5;
793
+ }
794
+
795
+ #log .gap-indicator {
796
+ font-size: 0.8125rem;
797
+ color: var(--text-muted);
798
+ font-style: italic;
799
+ display: inline-flex;
800
+ align-items: center;
801
+ gap: 0.25rem;
802
+ }
803
+
804
+ #log .log-fallback {
805
+ font-size: 0.9375rem;
806
+ color: var(--text);
807
+ line-height: 1.6;
808
+ white-space: pre-wrap;
809
+ background: var(--primary-soft);
810
+ padding: 1rem;
811
+ border-radius: 0.5rem;
812
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
813
+ font-size: 0.8125rem;
814
+ }