Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/agents/backend-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Router (HTTP) → Service (Business Logic) → Repository (Data Access) → Mode
## Rules

1. Stay in scope — only work on assigned backend tasks
2. Write tests for all new code
2. Write tests for all new code; honor the plan task's `test_approach` — for `tdd`, demonstrate RED before the change and record a `TDD_EVIDENCE` block (test command, RED, GREEN) in the result file
3. Follow Repository → Service → Router pattern (no business logic in routes)
4. Validate all inputs with the project's validation library
5. Parameterized queries only (no string interpolation in SQL)
Expand Down
2 changes: 1 addition & 1 deletion .agents/agents/debug-investigator.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ CHARTER_CHECK:
1. Stay in scope — only work on assigned debug tasks
2. Fix root cause, not symptoms
3. Minimal changes only — no refactoring during bugfix; route refactoring needs to refactor-engineer
4. Every fix gets a regression test
4. Every fix gets a regression test; run it before the fix where feasible and record RED (failing output) → GREEN (post-fix pass) in the bug report
5. Search for similar patterns after fixing
6. Document out-of-scope findings for other agents
7. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions
2 changes: 1 addition & 1 deletion .agents/agents/frontend-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ FSD-lite: root `src/` + feature `src/features/*/`
5. TailwindCSS v4 for styling, design tokens 1:1 mapping
6. Library defaults (greenfield; existing project choices win): luxon (dates), ahooks (hooks), es-toolkit (utils), jotai (client state), TanStack Query (server state)
7. Absolute imports with `@/`
8. Write tests for custom logic (>90% coverage target)
8. Write tests for custom logic (>90% coverage target); honor the plan task's `test_approach` — for `tdd`, demonstrate RED before the change and record a `TDD_EVIDENCE` block (test command, RED, GREEN) in the result file
9. Document out-of-scope dependencies for other agents
10. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions
2 changes: 1 addition & 1 deletion .agents/agents/mobile-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Clean Architecture: domain → data → presentation (Swift native: App/Core/Fea
5. Transport client with interceptors (Dio / axios / generated Client) + repository-layer response cache, offline-first architecture
6. Secrets in secure storage only — never plain prefs or MMKV
7. 60fps target performance
8. Write widget/component tests and integration tests
8. Write widget/component tests and integration tests; honor the plan task's `test_approach` — for `tdd`, demonstrate RED before the change and record a `TDD_EVIDENCE` block (test command, RED, GREEN) in the result file
9. ARB-based localization: edit ARB source files only, never generated localization code
10. Document out-of-scope dependencies for other agents
11. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions
3 changes: 2 additions & 1 deletion .agents/agents/pm-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ Each task must include:
- `priority`: execution tier — 1 = independent (runs first), 2 = depends on tier 1, etc. (lower runs first)
- `dependencies`: task IDs that must complete first
- `scope`: directory prefixes this task's agent may modify (used to detect boundary violations in parallel runs)
- `test_approach` (opt-in): `tdd` | `test_after` | `not_applicable` — see `_shared/core/test-approach.md`. `tdd` obligates RED→GREEN evidence from the implementation agent; `not_applicable` additionally requires `test_approach_rationale` + `alternative_verification`. Never assign `tdd` to refactor tasks (characterization tests instead)

## Rules

1. Stay in scope — planning only, no code implementation
2. API-first design
3. Minimize dependencies for maximum parallelism
4. Security and testing are part of every task (not separate)
4. Security and testing are part of every task (not separate); assign per-task `test_approach` (`tdd|test_after|not_applicable`) where a test strategy matters — `not_applicable` requires rationale + alternative verification, and no approach waives the >= 80% coverage gate
5. Each task completable by a single agent
6. Never modify `.agents/` files (SSOT) — run outputs under `.agents/results/` and `.agents/state/memories/` are the only exceptions
74 changes: 47 additions & 27 deletions .agents/hooks/core/keyword-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,20 +357,35 @@ export function escapeRegex(s: string): string {

/**
* Merge a language-keyed keyword/pattern bank into a single flat list:
* universal ("*") + English (the universal default) + the configured
* language's own entries (skipped when lang === "en" to avoid duplicates).
* Shared by buildPatterns and buildRawPatterns — both keyword banks and
* pattern banks use this exact `Record<string, string[]>` shape.
* universal ("*") + English + EVERY other language's entries, deduped
* case-insensitively. Same rationale as RC4 (buildInformationalPatterns):
* users prompt in whichever language they think in — `language` in
* oma-config.yaml controls the RESPONSE language, not the prompt language —
* so gating by config language silently disabled e.g. every Korean trigger
* for `language: en` projects. A keyword written in language X can only
* match a prompt that contains X-script text (current banks are en/ko/ja/zh;
* if a Latin-script bank like es/fr is ever added, phrase distinctiveness is
* the gate instead), so merging all languages cannot fire on unrelated
* prompts. Shared by buildPatterns and buildRawPatterns — both keyword banks
* and pattern banks use this exact `Record<string, string[]>` shape.
*/
export function collectLangEntries(
bank: Record<string, string[]>,
lang: string,
): string[] {
return [
export function collectLangEntries(bank: Record<string, string[]>): string[] {
const ordered = [
...(bank["*"] ?? []),
...(bank.en ?? []),
...(lang !== "en" ? (bank[lang] ?? []) : []),
...Object.entries(bank)
.filter(([key]) => key !== "*" && key !== "en")
.flatMap(([, entries]) => entries),
];
const seen = new Set<string>();
const out: string[] = [];
for (const entry of ordered) {
const key = entry.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
out.push(entry);
}
return out;
}

/**
Expand All @@ -390,7 +405,7 @@ export function buildPatternEntries(
lang: string,
cjkScripts: string[],
): KeywordPatternEntry[] {
return collectLangEntries(keywords, lang).map((kw) => {
return collectLangEntries(keywords).map((kw) => {
const escaped = escapeRegex(kw).replace(/\s+/g, "\\s+");
const regex =
cjkScripts.includes(lang) || /[^\p{ASCII}]/u.test(kw)
Expand Down Expand Up @@ -421,11 +436,10 @@ export interface RawPatternEntry {

export function buildRawPatternEntries(
patterns: Record<string, string[]> | undefined,
lang: string,
): RawPatternEntry[] {
if (!patterns) return [];
const compiled: RawPatternEntry[] = [];
for (const raw of collectLangEntries(patterns, lang)) {
for (const raw of collectLangEntries(patterns)) {
try {
compiled.push({ regex: new RegExp(raw, "iu"), source: raw });
} catch {
Expand All @@ -443,9 +457,8 @@ export function buildRawPatternEntries(
*/
export function buildRawPatterns(
patterns: Record<string, string[]> | undefined,
lang: string,
): RegExp[] {
return buildRawPatternEntries(patterns, lang).map((e) => e.regex);
return buildRawPatternEntries(patterns).map((e) => e.regex);
}

export function buildInformationalPatterns(config: TriggerConfig): RegExp[] {
Expand Down Expand Up @@ -707,6 +720,7 @@ function activateMode(
projectDir: string,
workflow: string,
sessionId: string,
omaSid?: string | null,
): void {
// Never persist a workflow under the unresolved-session fallback id: such a
// file cannot be isolated per session and would cross-contaminate any later
Expand All @@ -718,6 +732,7 @@ function activateMode(
sessionId,
activatedAt: new Date().toISOString(),
reinforcementCount: 0,
...(omaSid ? { omaSid } : {}),
};
writeFileSync(
join(getStateDir(projectDir), `${workflow}-state-${sessionId}.json`),
Expand Down Expand Up @@ -770,11 +785,12 @@ export const DEACTIVATION_PHRASES: Record<string, string[]> = {
pl: ["workflow zakończony", "workflow ukończony"],
};

export function isDeactivationRequest(prompt: string, lang: string): boolean {
const phrases = [
...(DEACTIVATION_PHRASES.en ?? []),
...(lang !== "en" ? (DEACTIVATION_PHRASES[lang] ?? []) : []),
];
export function isDeactivationRequest(prompt: string): boolean {
// All languages merged, never gated by config language (same rationale as
// collectLangEntries): a user prompting in Korean must be able to say
// "워크플로우 완료" even when `language: en`. A phrase only matches a prompt
// actually written in that language, so merging cannot misfire.
const phrases = Object.values(DEACTIVATION_PHRASES).flat();
const normalized = normalizeForMatching(prompt);
return phrases.some((phrase) =>
normalized.includes(normalizeForMatching(phrase)),
Expand Down Expand Up @@ -925,7 +941,7 @@ export async function run(
const lang = detectLanguage(projectDir);

// Check for deactivation request before workflow detection
if (isDeactivationRequest(prompt, lang)) {
if (isDeactivationRequest(prompt)) {
deactivateAllPersistentModes(projectDir, sessionId);
// Grok's resume context lives in a session-start file, not L1 stdout — clear it.
if (vendor === "grok") clearGrokContext(projectDir);
Expand Down Expand Up @@ -1025,10 +1041,7 @@ export async function run(
)) {
considerMatch(regex, keyword);
}
for (const { regex, source } of buildRawPatternEntries(
def.patterns,
lang,
)) {
for (const { regex, source } of buildRawPatternEntries(def.patterns)) {
considerMatch(regex, source);
}
}
Expand All @@ -1038,10 +1051,17 @@ export async function run(

const { workflow } = winner;

// Activate the L1 session first so its sid can be recorded in the
// persistent-mode state file (the Stop hook emits gate events under it).
const omaSid = await activateL1WorkflowSession(
projectDir,
workflow,
vendor,
sessionId,
);
if (winner.persistent) {
activateMode(projectDir, workflow, sessionId);
activateMode(projectDir, workflow, sessionId, omaSid);
}
await activateL1WorkflowSession(projectDir, workflow, vendor, sessionId);
const updatedState = recordKwTrigger(kwState, workflow);
saveKwState(projectDir, updatedState);

Expand Down
Loading