wogiflow 2.6.4 → 2.7.1

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 (32) hide show
  1. package/.claude/settings.json +1 -11
  2. package/lib/workspace-changelog.js +182 -0
  3. package/lib/workspace-channel-server.js +75 -2
  4. package/lib/workspace-contracts.js +151 -1
  5. package/lib/workspace-events.js +383 -0
  6. package/lib/workspace-gates.js +740 -0
  7. package/lib/workspace-integration-tests.js +299 -0
  8. package/lib/workspace-intelligence.js +486 -1
  9. package/lib/workspace-locks.js +371 -0
  10. package/lib/workspace-messages.js +203 -3
  11. package/lib/workspace-routing.js +147 -2
  12. package/lib/workspace.js +8 -0
  13. package/package.json +1 -1
  14. package/scripts/flow-done-gates.js +70 -0
  15. package/scripts/hooks/entry/claude-code/permission-denied.js +111 -0
  16. package/scripts/postinstall.js +64 -2
  17. package/.claude/rules/_internal/README.md +0 -64
  18. package/.claude/rules/_internal/document-structure.md +0 -77
  19. package/.claude/rules/_internal/dual-repo-management.md +0 -174
  20. package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
  21. package/.claude/rules/_internal/github-releases.md +0 -71
  22. package/.claude/rules/_internal/model-management.md +0 -35
  23. package/.claude/rules/_internal/self-maintenance.md +0 -87
  24. package/.claude/rules/architecture/component-reuse.md +0 -38
  25. package/.claude/rules/code-style/naming-conventions.md +0 -107
  26. package/.claude/rules/operations/git-workflows.md +0 -92
  27. package/.claude/rules/operations/scratch-directory.md +0 -54
  28. package/.claude/rules/security/security-patterns.md +0 -176
  29. package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
  30. package/.workflow/specs/architecture.md.template +0 -24
  31. package/.workflow/specs/stack.md.template +0 -33
  32. package/.workflow/specs/testing.md.template +0 -36
@@ -221,8 +221,8 @@ After completing the task:
221
221
 
222
222
  agentConfig: {
223
223
  description: `${repoName}: ${task.substring(0, 50)}...`,
224
- // The sub-agent should work within the repo directory
225
- // The orchestrator (workspace manager) will read results after completion
224
+ // Named subagent (2.1.88+): shows in @ mention typeahead as @repoName
225
+ name: repoName
226
226
  }
227
227
  };
228
228
  }
@@ -364,6 +364,7 @@ Your job:
364
364
  Be specific about file names, line numbers, and error messages.`,
365
365
  agentConfig: {
366
366
  description: `Investigate: ${name} — ${bugDescription.substring(0, 40)}...`,
367
+ name: `${name}-investigator`,
367
368
  model: 'sonnet' // Use cheaper model for investigation
368
369
  }
369
370
  });
@@ -479,6 +480,146 @@ function getExecutionOrder(manifest, targetRepos) {
479
480
  return order;
480
481
  }
481
482
 
483
+ // ============================================================
484
+ // Cross-Repo Dependency-Aware Task Blocking
485
+ // ============================================================
486
+
487
+ /**
488
+ * Block consumer-side tasks until their provider dependencies are complete.
489
+ * Reads workspace-level ready.json and each member's ready.json to find
490
+ * cross-repo dependencies.
491
+ *
492
+ * @param {string} workspaceRoot
493
+ * @param {Object} manifest
494
+ * @returns {{ blockedTasks: Array<Object>, unblockedTasks: Array<Object> }}
495
+ */
496
+ function updateCrossRepoBlocking(workspaceRoot, manifest) {
497
+ const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
498
+ let config;
499
+ try {
500
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
501
+ } catch (_err) {
502
+ return { blockedTasks: [], unblockedTasks: [] };
503
+ }
504
+
505
+ const blockedTasks = [];
506
+ const unblockedTasks = [];
507
+ const memberTasks = {};
508
+
509
+ // Collect all tasks from all member repos
510
+ for (const [name, memberConfig] of Object.entries(config.members || {})) {
511
+ const memberPath = path.resolve(workspaceRoot, memberConfig.path);
512
+ const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
513
+ try {
514
+ if (fs.existsSync(readyPath)) {
515
+ const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
516
+ memberTasks[name] = {
517
+ path: memberPath,
518
+ readyPath,
519
+ ready,
520
+ inProgress: ready.inProgress || [],
521
+ readyItems: ready.ready || [],
522
+ completed: ready.recentlyCompleted || []
523
+ };
524
+ }
525
+ } catch (_err) {
526
+ // Skip
527
+ }
528
+ }
529
+
530
+ // For each member's ready tasks, check if they depend on workspace tasks
531
+ const executionOrder = getExecutionOrder(manifest, Object.keys(config.members));
532
+
533
+ for (const [name, data] of Object.entries(memberTasks)) {
534
+ const memberPhase = executionOrder.find(o => o.name === name);
535
+ if (!memberPhase) continue;
536
+
537
+ for (const task of data.readyItems) {
538
+ // Check if this task has workspace source and blockedBy
539
+ if (!task.source?.startsWith('workspace:')) continue;
540
+
541
+ const blockedBy = task.blockedBy || [];
542
+ let isBlocked = false;
543
+
544
+ for (const depId of blockedBy) {
545
+ // Check if the blocking task is completed in any member
546
+ let depCompleted = false;
547
+ for (const [depName, depData] of Object.entries(memberTasks)) {
548
+ if (depData.completed.some(t => t.id === depId)) {
549
+ depCompleted = true;
550
+ break;
551
+ }
552
+ }
553
+
554
+ if (!depCompleted) {
555
+ isBlocked = true;
556
+ break;
557
+ }
558
+ }
559
+
560
+ if (isBlocked) {
561
+ blockedTasks.push({ repo: name, task, blockedBy });
562
+ } else if (blockedBy.length > 0) {
563
+ unblockedTasks.push({ repo: name, task });
564
+ }
565
+ }
566
+ }
567
+
568
+ return { blockedTasks, unblockedTasks };
569
+ }
570
+
571
+ /**
572
+ * Build a visual dependency tree for workspace tasks.
573
+ *
574
+ * @param {string} workspaceRoot
575
+ * @param {Object} manifest
576
+ * @returns {string} formatted tree
577
+ */
578
+ function formatDependencyTree(workspaceRoot, manifest) {
579
+ const configPath = path.join(workspaceRoot, 'wogi-workspace.json');
580
+ let config;
581
+ try {
582
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
583
+ } catch (_err) {
584
+ return 'No workspace config found.';
585
+ }
586
+
587
+ const lines = ['Workspace Task Dependencies:'];
588
+ const order = getExecutionOrder(manifest, Object.keys(config.members || {}));
589
+
590
+ for (const entry of order) {
591
+ const memberConfig = config.members[entry.name];
592
+ if (!memberConfig) continue;
593
+
594
+ const memberPath = path.resolve(workspaceRoot, memberConfig.path);
595
+ const readyPath = path.join(memberPath, '.workflow', 'state', 'ready.json');
596
+
597
+ try {
598
+ if (!fs.existsSync(readyPath)) continue;
599
+ const ready = JSON.parse(fs.readFileSync(readyPath, 'utf-8'));
600
+ const wsTasks = [...(ready.inProgress || []), ...(ready.ready || []), ...(ready.blocked || [])]
601
+ .filter(t => t.source?.startsWith('workspace:'));
602
+
603
+ if (wsTasks.length === 0) continue;
604
+
605
+ lines.push(`\n ${entry.name} (${entry.role}, phase ${entry.order}):`);
606
+ for (const task of wsTasks) {
607
+ const status = task.status === 'completed' ? '\u2713' :
608
+ (ready.inProgress || []).some(t => t.id === task.id) ? '\u25B6' :
609
+ task.blockedBy?.length ? '\u2718' : '\u25CB';
610
+ const blockedNote = task.blockedBy?.length
611
+ ? ` [blocked by: ${task.blockedBy.join(', ')}]`
612
+ : '';
613
+ lines.push(` ${status} ${task.id} — ${task.title}${blockedNote}`);
614
+ }
615
+ } catch (_err) {
616
+ // Skip
617
+ }
618
+ }
619
+
620
+ return lines.join('\n');
621
+ }
622
+
482
623
  // ============================================================
483
624
  // Channel-Based Dispatch (wf-d4b98f60)
484
625
  // ============================================================
@@ -772,6 +913,10 @@ module.exports = {
772
913
  // Ordering
773
914
  getExecutionOrder,
774
915
 
916
+ // Cross-repo blocking
917
+ updateCrossRepoBlocking,
918
+ formatDependencyTree,
919
+
775
920
  // Channel dispatch
776
921
  dispatchToChannel,
777
922
  dispatchCrossRepoPlan,
package/lib/workspace.js CHANGED
@@ -637,6 +637,14 @@ ${Object.entries(config.channels?.members || {}).map(([name, ch]) =>
637
637
  \`\`\`
638
638
  Then read responses from \`.workspace/messages/\` and synthesize findings.
639
639
 
640
+ ## Named Workspace Agents (Claude Code 2.1.88+)
641
+
642
+ Workers are named subagents — they appear in the @ mention typeahead. Users can address them directly:
643
+ ${Object.keys(config.channels?.members || {}).map(name => `- **@${name}** — the ${name} repo worker`).join('\n')}
644
+ ${Object.keys(config.channels?.members || {}).map(name => `- **@${name}-investigator** — investigation agent for ${name}`).join('\n')}
645
+
646
+ When spawning agents for delegation, always include the \`name\` field in the Agent config to enable @mention addressing.
647
+
640
648
  ## Waiting for Worker Results (CRITICAL — Automatic Return Path)
641
649
 
642
650
  Workers **automatically write results** to \`.workspace/messages/\` when they complete a task. You do NOT need to ask them to report back — the task-completed hook writes a \`task-complete\` message automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.6.4",
3
+ "version": "2.7.1",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -57,6 +57,19 @@ function getRegistryManager() {
57
57
  return _registryManagerModule;
58
58
  }
59
59
 
60
+ // Workspace gates — lazy-loaded (only active when workspace mode is detected)
61
+ let _workspaceGatesModule = undefined;
62
+ function getWorkspaceGates() {
63
+ if (_workspaceGatesModule === undefined) {
64
+ try {
65
+ _workspaceGatesModule = require('../lib/workspace-gates');
66
+ } catch (_err) {
67
+ _workspaceGatesModule = null;
68
+ }
69
+ }
70
+ return _workspaceGatesModule;
71
+ }
72
+
60
73
  // ============================================================
61
74
  // Gate Handlers
62
75
  // ============================================================
@@ -717,6 +730,60 @@ function unknownGate(ctx, gateName) {
717
730
  // Gate Registry
718
731
  // ============================================================
719
732
 
733
+ // ============================================================
734
+ // Workspace Quality Gates (conditional — only when workspace active)
735
+ // ============================================================
736
+
737
+ /**
738
+ * Workspace gate handler. Delegates to workspace-gates.js for each
739
+ * sub-gate (crossRepoImpactCheck, contractCompliance, peerNotification,
740
+ * cascadeVerification, integrationMapFreshness).
741
+ *
742
+ * This is a single gate entry in GATE_REGISTRY that runs all applicable
743
+ * workspace sub-gates based on task type.
744
+ */
745
+ function workspaceGate(ctx, gateName) {
746
+ const wsGates = getWorkspaceGates();
747
+ if (!wsGates) {
748
+ return { passed: true, skipped: true };
749
+ }
750
+
751
+ const ws = wsGates.workspaceActive();
752
+ if (!ws.active) {
753
+ return { passed: true, skipped: true };
754
+ }
755
+
756
+ const context = wsGates.loadWorkspaceContext(ws.root);
757
+ const taskMeta = {
758
+ taskId: ctx.taskId,
759
+ taskTitle: ctx.taskTitle || '',
760
+ taskType: ctx.normalizedType || 'feature',
761
+ impactAssessed: ctx.impactAssessed || false
762
+ };
763
+
764
+ const results = wsGates.runAllWorkspaceGates(ws.root, context, taskMeta);
765
+
766
+ // Display results
767
+ for (const r of results.results) {
768
+ if (r.passed) {
769
+ ctx.success(`workspace/${r.gate}: ${r.message}`);
770
+ } else if (r.severity === 'warning') {
771
+ console.log(` ${ctx.color('yellow', '\u25CB')} workspace/${r.gate}: ${r.message}`);
772
+ } else {
773
+ ctx.error(`workspace/${r.gate}: ${r.message}`);
774
+ }
775
+ }
776
+
777
+ if (!results.passed) {
778
+ return {
779
+ passed: false,
780
+ errorOutput: `${results.errors} workspace gate(s) failed, ${results.warnings} warning(s)`
781
+ };
782
+ }
783
+
784
+ return { passed: true };
785
+ }
786
+
720
787
  const GATE_REGISTRY = {
721
788
  tests: testsGate,
722
789
  lint: lintGate,
@@ -737,6 +804,8 @@ const GATE_REGISTRY = {
737
804
  uiVerification: verificationGate,
738
805
  apiVerification: verificationGate,
739
806
  testDiscovery: testDiscoveryGate,
807
+ // Workspace gates (conditional — auto-skip when not in workspace)
808
+ workspaceCompliance: workspaceGate,
740
809
  };
741
810
 
742
811
  /**
@@ -782,5 +851,6 @@ module.exports = {
782
851
  generatedTestsPassGate,
783
852
  verificationGate,
784
853
  testDiscoveryGate,
854
+ workspaceGate,
785
855
  unknownGate,
786
856
  };
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Claude Code PermissionDenied Hook
5
+ *
6
+ * Fires after auto-mode classifier denies a tool use.
7
+ * Available in Claude Code 2.1.88+.
8
+ *
9
+ * Handles:
10
+ * 1. Logging — tracks denied permissions for diagnostics
11
+ * 2. Workspace redirect — when a worker tries to access a file in another
12
+ * repo, redirect via the workspace message bus instead of retrying
13
+ * 3. Guidance — provides actionable hints about what to do instead
14
+ *
15
+ * Return { retry: true } to tell the model it can retry the operation.
16
+ * Return { retry: false } (or nothing) to accept the denial.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const path = require('node:path');
22
+ const { runHook } = require('../shared/hook-runner');
23
+
24
+ runHook('PermissionDenied', async ({ input, parsedInput }) => {
25
+ const toolName = parsedInput.toolName || input.tool_name || 'unknown';
26
+ const toolInput = parsedInput.toolInput || input.tool_input || {};
27
+ const filePath = toolInput.file_path || toolInput.command || '';
28
+ const reason = input.denial_reason || input.reason || '';
29
+
30
+ // ── 1. Log the denial for diagnostics ──────────────────────
31
+ try {
32
+ const fs = require('node:fs');
33
+ const { PATHS } = require('../../../flow-utils');
34
+ const logPath = path.join(PATHS.state, 'permission-denials.json');
35
+
36
+ let denials = [];
37
+ try {
38
+ if (fs.existsSync(logPath)) {
39
+ denials = JSON.parse(fs.readFileSync(logPath, 'utf-8'));
40
+ if (!Array.isArray(denials)) denials = [];
41
+ }
42
+ } catch (_err) {
43
+ denials = [];
44
+ }
45
+
46
+ denials.push({
47
+ tool: toolName,
48
+ target: typeof filePath === 'string' ? filePath.substring(0, 200) : '',
49
+ reason: typeof reason === 'string' ? reason.substring(0, 200) : '',
50
+ timestamp: new Date().toISOString()
51
+ });
52
+
53
+ // Keep last 100 denials
54
+ if (denials.length > 100) denials = denials.slice(-100);
55
+
56
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
57
+ fs.writeFileSync(logPath, JSON.stringify(denials, null, 2));
58
+ } catch (_err) {
59
+ // Non-critical — don't let logging failure break the hook
60
+ }
61
+
62
+ // ── 2. Workspace redirect — cross-repo file access ─────────
63
+ // When a worker tries to read/write a file that belongs to another repo,
64
+ // redirect them to use the workspace message bus instead.
65
+ const isWorkspace = !!process.env.WOGI_WORKSPACE_ROOT;
66
+ const repoName = process.env.WOGI_REPO_NAME || '';
67
+
68
+ if (isWorkspace && typeof filePath === 'string' && filePath.length > 0) {
69
+ const workspaceRoot = process.env.WOGI_WORKSPACE_ROOT;
70
+
71
+ // Check if the denied path is inside the workspace but outside this repo
72
+ try {
73
+ const resolvedFile = path.resolve(filePath);
74
+ const resolvedRoot = path.resolve(workspaceRoot);
75
+ const isInsideWorkspace = resolvedFile.startsWith(resolvedRoot + path.sep);
76
+ const cwd = process.cwd();
77
+ const isOutsideOwnRepo = !resolvedFile.startsWith(cwd + path.sep) && resolvedFile !== cwd;
78
+
79
+ if (isInsideWorkspace && isOutsideOwnRepo) {
80
+ // This is a cross-repo access attempt — redirect to message bus
81
+ // Extract the target repo name from the path
82
+ const relPath = resolvedFile.substring(resolvedRoot.length + 1);
83
+ const targetRepo = relPath.split(path.sep)[0] || 'unknown';
84
+
85
+ const guidance = [
86
+ `Cross-repo file access denied: ${toolName} on ${path.basename(filePath)}`,
87
+ `Target repo: ${targetRepo} (you are: ${repoName})`,
88
+ '',
89
+ 'In workspace mode, repos cannot directly access each other\'s files.',
90
+ `Use workspace_send_message(to: "${targetRepo}", message: "...") to ask the other repo\'s worker.`,
91
+ `Or use workspace_send_message(to: "manager", message: "...") to escalate to the manager.`
92
+ ].join('\n');
93
+
94
+ return {
95
+ __raw: true,
96
+ retry: false,
97
+ message: guidance
98
+ };
99
+ }
100
+ } catch (_err) {
101
+ // Path resolution failed — fall through to default handling
102
+ }
103
+ }
104
+
105
+ // ── 3. Default handling — accept denial with guidance ──────
106
+ // For non-workspace denials, just accept and let the model know
107
+ return {
108
+ __raw: true,
109
+ retry: false
110
+ };
111
+ }, { failMode: 'silent' });
@@ -269,6 +269,8 @@ const HOOK_VERSION_MAP = {
269
269
  PostCompact: { major: 2, minor: 1, patch: 76 },
270
270
  // Hooks added in 2.1.84+
271
271
  TaskCreated: { major: 2, minor: 1, patch: 84 },
272
+ // Hooks added in 2.1.88+
273
+ PermissionDenied: { major: 2, minor: 1, patch: 88 },
272
274
  };
273
275
 
274
276
  /**
@@ -310,7 +312,26 @@ function versionMeetsMinimum(version, minimum) {
310
312
  * @returns {string[]} List of removed hook names
311
313
  */
312
314
  function stripUnsupportedHooks(settings, ccVersion) {
313
- if (!settings || !settings.hooks || !ccVersion) return [];
315
+ if (!settings || !settings.hooks) return [];
316
+
317
+ // CRITICAL: When CC version is unknown, strip ALL hooks added after the base set (2.1.23).
318
+ // CC rejects the ENTIRE settings file if it encounters an unknown hook event name —
319
+ // this means one unsupported hook kills ALL hooks. Fail-safe: keep only the base set.
320
+ const BASE_HOOKS = new Set([
321
+ 'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse',
322
+ 'Stop', 'SessionEnd', 'TaskCompleted'
323
+ ]);
324
+
325
+ if (!ccVersion) {
326
+ const removed = [];
327
+ for (const hookName of Object.keys(settings.hooks)) {
328
+ if (!BASE_HOOKS.has(hookName)) {
329
+ delete settings.hooks[hookName];
330
+ removed.push(`${hookName} (CC version unknown — keeping base hooks only)`);
331
+ }
332
+ }
333
+ return removed;
334
+ }
314
335
 
315
336
  const removed = [];
316
337
  for (const hookName of Object.keys(settings.hooks)) {
@@ -323,6 +344,39 @@ function stripUnsupportedHooks(settings, ccVersion) {
323
344
  return removed;
324
345
  }
325
346
 
347
+ /**
348
+ * Dynamically inject hooks that are NOT in the base settings.json but are
349
+ * supported by the detected Claude Code version. These hooks are intentionally
350
+ * excluded from the committed settings.json because CC rejects the ENTIRE file
351
+ * when it encounters an unknown hook event name.
352
+ *
353
+ * @param {Object} settings - Parsed settings object (mutated in place)
354
+ * @param {{ major: number, minor: number, patch: number }} ccVersion
355
+ */
356
+ function injectDynamicHooks(settings, ccVersion) {
357
+ if (!settings || !settings.hooks) return;
358
+
359
+ // Map of hooks to inject with their minimum version and command
360
+ const DYNAMIC_HOOKS = [
361
+ {
362
+ name: 'TaskCreated',
363
+ minVersion: { major: 2, minor: 1, patch: 84 },
364
+ config: [{ hooks: [{ type: 'command', command: 'node scripts/hooks/entry/claude-code/task-created.js', timeout: 5 }] }]
365
+ },
366
+ {
367
+ name: 'PermissionDenied',
368
+ minVersion: { major: 2, minor: 1, patch: 88 },
369
+ config: [{ hooks: [{ type: 'command', command: 'node scripts/hooks/entry/claude-code/permission-denied.js', timeout: 5 }] }]
370
+ }
371
+ ];
372
+
373
+ for (const hook of DYNAMIC_HOOKS) {
374
+ if (!settings.hooks[hook.name] && versionMeetsMinimum(ccVersion, hook.minVersion)) {
375
+ settings.hooks[hook.name] = hook.config;
376
+ }
377
+ }
378
+ }
379
+
326
380
  /**
327
381
  * Rewrite hook command paths from local dev paths to package paths.
328
382
  * The package's settings.json uses local paths (node scripts/hooks/...)
@@ -404,12 +458,20 @@ function copyClaudeResources() {
404
458
  const ccVersion = detectClaudeCodeVersion();
405
459
  const removedHooks = stripUnsupportedHooks(ours, ccVersion);
406
460
  if (removedHooks.length > 0) {
407
- console.log(`[wogiflow] Claude Code ${ccVersion.major}.${ccVersion.minor}.${ccVersion.patch} detected. Excluded unsupported hooks:`);
461
+ const versionStr = ccVersion ? `${ccVersion.major}.${ccVersion.minor}.${ccVersion.patch}` : 'unknown';
462
+ console.log(`[wogiflow] Claude Code ${versionStr} detected. Excluded unsupported hooks:`);
408
463
  for (const h of removedHooks) {
409
464
  console.log(` - ${h}`);
410
465
  }
411
466
  console.log('[wogiflow] Update Claude Code for full functionality: npm i -g @anthropic-ai/claude-code@latest');
412
467
  }
468
+
469
+ // Dynamically inject hooks supported by this CC version but not in the base settings.json.
470
+ // These hooks are NOT committed to the repo to avoid breaking older CC versions.
471
+ if (ccVersion) {
472
+ injectDynamicHooks(ours, ccVersion);
473
+ }
474
+
413
475
  // Always update hooks (core WogiFlow functionality)
414
476
  existing.hooks = ours.hooks;
415
477
  existing._wogiFlowManaged = true;
@@ -1,64 +0,0 @@
1
- ---
2
- alwaysApply: false
3
- description: "Meta-documentation about how project rules are organized"
4
- ---
5
- # Project Rules
6
-
7
- This directory contains coding rules and patterns for this project, organized by category.
8
-
9
- ## Structure
10
-
11
- ```
12
- .claude/rules/
13
- ├── code-style/ # Naming conventions, formatting
14
- │ └── naming-conventions.md
15
- ├── security/ # Security patterns and practices
16
- │ └── security-patterns.md
17
- ├── architecture/ # Design decisions and patterns
18
- │ ├── component-reuse.md
19
- │ └── model-management.md
20
- └── README.md
21
- ```
22
-
23
- ## How Rules Work
24
-
25
- Rules are automatically loaded by Claude Code based on:
26
- - **alwaysApply: true** - Rule is always loaded
27
- - **alwaysApply: false** - Rule is loaded based on `globs` or `description` relevance
28
- - **globs** - File patterns that trigger rule loading
29
-
30
- ## Adding New Rules
31
-
32
- 1. Choose the appropriate category subdirectory
33
- 2. Create a `.md` file with frontmatter:
34
-
35
- ```yaml
36
- ---
37
- alwaysApply: false
38
- description: "Brief description for relevance matching"
39
- globs: src/**/*.ts # Optional: only load for these files
40
- ---
41
- ```
42
-
43
- 3. Write the rule content in markdown
44
-
45
- ## Categories
46
-
47
- | Category | Purpose |
48
- |----------|---------|
49
- | code-style | Naming conventions, formatting, file structure |
50
- | security | Security patterns, input validation, safe practices |
51
- | architecture | Design decisions, component patterns, system organization |
52
-
53
- ## Auto-Generation
54
-
55
- Some rules can be auto-generated from `.workflow/state/decisions.md`:
56
-
57
- ```bash
58
- node scripts/flow-rules-sync.js
59
- ```
60
-
61
- The sync script will route rules to appropriate category subdirectories.
62
-
63
- ---
64
- Last updated: 2026-01-12
@@ -1,77 +0,0 @@
1
- ---
2
- alwaysApply: false
3
- description: "All AI-context documents must use PIN markers for targeted context loading"
4
- globs: ".workflow/**/*.md"
5
- ---
6
-
7
- # Document Structure for AI Context
8
-
9
- All documents in `.workflow/` that are used as AI context MUST follow the PIN standard.
10
-
11
- ## Required Structure
12
-
13
- ### 1. Header with PIN List
14
- Every document starts with a comment listing all pins in the document:
15
- ```markdown
16
- <!-- PINS: pin1, pin2, pin3 -->
17
- ```
18
-
19
- ### 2. Section PIN Markers
20
- Each major section has a PIN marker comment:
21
- ```markdown
22
- ### Section Title
23
- <!-- PIN: section-specific-pin -->
24
- [Content]
25
- ```
26
-
27
- ### 3. PIN Naming Convention
28
- - Use kebab-case: `user-authentication`, not `userAuthentication`
29
- - Use semantic names: `error-handling`, not `eh`
30
- - Use compound names for specificity: `json-parse-safety`
31
-
32
- ## Why PINs Matter
33
-
34
- The PIN system enables:
35
- 1. **Targeted context loading**: Only load sections relevant to current task
36
- 2. **Cheaper model routing**: Haiku can fetch only relevant sections for Opus
37
- 3. **Change detection**: Hash sections independently for smart invalidation
38
- 4. **Cross-reference**: Link sections by PIN across documents
39
-
40
- ## Example Document
41
-
42
- ```markdown
43
- # Config Reference
44
-
45
- <!-- PINS: database, authentication, api-keys, environment -->
46
-
47
- ## Database Settings
48
- <!-- PIN: database -->
49
- | Setting | Default | Description |
50
- |---------|---------|-------------|
51
-
52
- ## Authentication
53
- <!-- PIN: authentication -->
54
- | Setting | Default | Description |
55
- |---------|---------|-------------|
56
- ```
57
-
58
- ## Parsing
59
-
60
- The PIN system automatically parses documents with:
61
- - `flow-section-index.js` - Generates section index with pins
62
- - `flow-section-resolver.js` - Resolves sections by PIN lookup
63
- - `getSectionsByPins(['auth', 'security'])` - Fetch only relevant sections
64
-
65
- ## Files That Must Have PINs
66
-
67
- | File | Required PINs |
68
- |------|---------------|
69
- | `decisions.md` | Per coding rule/pattern |
70
- | `app-map.md` | Per component/screen |
71
- | `product.md` | Per product section |
72
- | `stack.md` | Per technology |
73
-
74
- ## Validation
75
-
76
- Run `node scripts/flow-section-index.js --force` to regenerate the index.
77
- Check `.workflow/state/section-index.json` for indexed sections and their pins.