universal-dev-standards 6.8.0 → 6.10.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.
- package/bin/uds.js +12 -2
- package/bundled/ai/standards/acceptance-criteria-traceability.ai.yaml +14 -2
- package/bundled/ai/standards/adr-standards.ai.yaml +14 -2
- package/bundled/ai/standards/code-review.ai.yaml +13 -3
- package/bundled/ai/standards/commit-message.ai.yaml +8 -4
- package/bundled/ai/standards/deferred-item-exit.ai.yaml +225 -0
- package/bundled/ai/standards/feature-discovery-standards.ai.yaml +14 -2
- package/bundled/ai/standards/governance-layer.ai.yaml +128 -2
- package/bundled/ai/standards/logging.ai.yaml +2 -2
- package/bundled/ai/standards/retrospective-standards.ai.yaml +14 -2
- package/bundled/ai/standards/reverse-engineering-standards.ai.yaml +73 -2
- package/bundled/ai/standards/security-standards.ai.yaml +2 -2
- package/bundled/ai/standards/spec-driven-development.ai.yaml +14 -2
- package/bundled/ai/standards/tech-debt-standards.ai.yaml +87 -3
- package/bundled/ai/standards/turn-completion-integrity.ai.yaml +131 -0
- package/bundled/core/acceptance-criteria-traceability.md +5 -2
- package/bundled/core/adr-standards.md +26 -2
- package/bundled/core/agent-communication-protocol.md +8 -0
- package/bundled/core/branch-completion.md +8 -0
- package/bundled/core/change-batching-standards.md +8 -0
- package/bundled/core/code-review-checklist.md +5 -2
- package/bundled/core/context-aware-loading.md +1 -1
- package/bundled/core/deferred-item-exit.md +254 -0
- package/bundled/core/execution-history.md +8 -0
- package/bundled/core/feature-discovery-standards.md +5 -1
- package/bundled/core/governance-layer.md +114 -2
- package/bundled/core/pipeline-integration-standards.md +8 -0
- package/bundled/core/retrospective-standards.md +4 -2
- package/bundled/core/reverse-engineering-standards.md +81 -2
- package/bundled/core/spec-driven-development.md +8 -2
- package/bundled/core/tech-debt-standards.md +67 -8
- package/bundled/core/turn-completion-integrity.md +196 -0
- package/bundled/core/workflow-enforcement.md +8 -0
- package/bundled/core/workflow-state-protocol.md +8 -0
- package/bundled/hooks/check-dangerous-cmd.mjs +60 -0
- package/bundled/hooks/check-logging-standard.mjs +59 -0
- package/bundled/hooks/check-turn-completion.mjs +233 -0
- package/bundled/hooks/inject-standards.mjs +183 -0
- package/bundled/hooks/telemetry-wrapper.mjs +77 -0
- package/bundled/hooks/turn-completion/detect.mjs +99 -0
- package/bundled/hooks/turn-completion/locales/en.mjs +159 -0
- package/bundled/hooks/turn-completion/locales/zh-TW.mjs +166 -0
- package/bundled/hooks/validate-commit-msg.mjs +104 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +73 -3
- package/bundled/locales/zh-CN/CLAUDE.md +1 -1
- package/bundled/locales/zh-CN/README.md +2 -2
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/core/adr-standards.md +1 -1
- package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
- package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-CN/core/execution-history.md +7 -0
- package/bundled/locales/zh-CN/core/governance-layer.md +118 -6
- package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-CN/core/retrospective-standards.md +1 -1
- package/bundled/locales/zh-CN/core/tech-debt-standards.md +71 -4
- package/bundled/locales/zh-CN/core/turn-completion-integrity.md +190 -0
- package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +8 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +29 -68
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +25 -15
- package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-CN/docs/USAGE-MODES-COMPARISON.md +1 -2
- package/bundled/locales/zh-CN/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
- package/bundled/locales/zh-CN/integrations/google-antigravity/README.md +3 -3
- package/bundled/locales/zh-CN/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/brainstorm-assistant/SKILL.md +22 -12
- package/bundled/locales/zh-CN/skills/brainstorm-assistant/guide.md +12 -9
- package/bundled/locales/zh-CN/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/commands/brainstorm.md +17 -13
- package/bundled/locales/zh-CN/skills/commands/config.md +0 -1
- package/bundled/locales/zh-CN/skills/commands/init.md +1 -2
- package/bundled/locales/zh-CN/skills/commit-standards/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/contract-test-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/dev-methodology/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/release-standards/SKILL.md +3 -0
- package/bundled/locales/zh-CN/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-CN/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/locales/zh-CN/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-CN/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/CHANGELOG.md +74 -3
- package/bundled/locales/zh-TW/CLAUDE.md +1 -1
- package/bundled/locales/zh-TW/README.md +2 -2
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/core/acceptance-criteria-traceability.md +2 -0
- package/bundled/locales/zh-TW/core/adr-standards.md +26 -5
- package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
- package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
- package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
- package/bundled/locales/zh-TW/core/code-review-checklist.md +2 -0
- package/bundled/locales/zh-TW/core/container-image-standards.md +2 -2
- package/bundled/locales/zh-TW/core/contract-testing-standards.md +2 -2
- package/bundled/locales/zh-TW/core/cross-flow-regression.md +8 -7
- package/bundled/locales/zh-TW/core/data-contract.md +2 -2
- package/bundled/locales/zh-TW/core/data-migration-testing.md +2 -2
- package/bundled/locales/zh-TW/core/data-pipeline.md +2 -2
- package/bundled/locales/zh-TW/core/deferred-item-exit.md +251 -0
- package/bundled/locales/zh-TW/core/documentation-writing-standards.md +228 -3
- package/bundled/locales/zh-TW/core/execution-history.md +7 -0
- package/bundled/locales/zh-TW/core/full-coverage-testing.md +15 -2
- package/bundled/locales/zh-TW/core/governance-layer.md +118 -5
- package/bundled/locales/zh-TW/core/iac-design-principles.md +2 -2
- package/bundled/locales/zh-TW/core/incident-response.md +2 -2
- package/bundled/locales/zh-TW/core/model-provenance.md +4 -2
- package/bundled/locales/zh-TW/core/pii-classification.md +42 -6
- package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
- package/bundled/locales/zh-TW/core/prd-standards.md +4 -2
- package/bundled/locales/zh-TW/core/product-metrics-standards.md +4 -2
- package/bundled/locales/zh-TW/core/release-readiness-gate.md +2 -2
- package/bundled/locales/zh-TW/core/resource-cost-boundary.md +2 -2
- package/bundled/locales/zh-TW/core/retrospective-standards.md +5 -3
- package/bundled/locales/zh-TW/core/reverse-engineering-standards.md +83 -5
- package/bundled/locales/zh-TW/core/runbook.md +2 -2
- package/bundled/locales/zh-TW/core/schema-evolution.md +2 -2
- package/bundled/locales/zh-TW/core/secret-management-standards.md +2 -2
- package/bundled/locales/zh-TW/core/slo-sli.md +2 -2
- package/bundled/locales/zh-TW/core/spec-driven-development.md +2 -0
- package/bundled/locales/zh-TW/core/tech-debt-standards.md +71 -4
- package/bundled/locales/zh-TW/core/turn-completion-integrity.md +190 -0
- package/bundled/locales/zh-TW/core/user-journey-testing.md +2 -2
- package/bundled/locales/zh-TW/core/user-story-mapping.md +2 -2
- package/bundled/locales/zh-TW/core/verification-oracle.md +2 -2
- package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
- package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +8 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +29 -68
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +25 -15
- package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
- package/bundled/locales/zh-TW/docs/USAGE-MODES-COMPARISON.md +1 -2
- package/bundled/locales/zh-TW/integrations/google-antigravity/{INSTRUCTIONS.md → AGENTS.md} +1 -1
- package/bundled/locales/zh-TW/integrations/google-antigravity/README.md +3 -3
- package/bundled/locales/zh-TW/skills/adr-assistant/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/brainstorm-assistant/SKILL.md +22 -12
- package/bundled/locales/zh-TW/skills/brainstorm-assistant/guide.md +12 -9
- package/bundled/locales/zh-TW/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/commands/brainstorm.md +17 -13
- package/bundled/locales/zh-TW/skills/commands/config.md +0 -1
- package/bundled/locales/zh-TW/skills/commands/init.md +1 -2
- package/bundled/locales/zh-TW/skills/commit-standards/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/contract-test-assistant/SKILL.md +2 -1
- package/bundled/locales/zh-TW/skills/dev-methodology/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/dev-workflow-guide/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/knowledge-graph/guide.md +2 -2
- package/bundled/locales/zh-TW/skills/migration-assistant/SKILL.md +1 -1
- package/bundled/locales/zh-TW/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/project-discovery/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/release-standards/SKILL.md +3 -0
- package/bundled/locales/zh-TW/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/locales/zh-TW/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/locales/zh-TW/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/locales/zh-TW/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/skills/atdd-assistant/SKILL.md +2 -0
- package/bundled/skills/bdd-assistant/SKILL.md +2 -0
- package/bundled/skills/brainstorm-assistant/SKILL.md +31 -13
- package/bundled/skills/brainstorm-assistant/guide.md +9 -6
- package/bundled/skills/code-review-assistant/SKILL.md +1 -0
- package/bundled/skills/commands/brainstorm.md +12 -9
- package/bundled/skills/commands/config.md +0 -1
- package/bundled/skills/commands/init.md +2 -3
- package/bundled/skills/commit-standards/SKILL.md +2 -0
- package/bundled/skills/contract-test-assistant/SKILL.md +1 -0
- package/bundled/skills/dev-methodology/SKILL.md +4 -0
- package/bundled/skills/observability-assistant/SKILL.md +1 -0
- package/bundled/skills/project-discovery/SKILL.md +1 -0
- package/bundled/skills/project-structure-guide/SKILL.md +1 -0
- package/bundled/skills/release-standards/SKILL.md +3 -0
- package/bundled/skills/requirement-assistant/SKILL.md +2 -0
- package/bundled/skills/reverse-engineer/SKILL.md +3 -0
- package/bundled/skills/runbook-assistant/SKILL.md +1 -0
- package/bundled/skills/slo-assistant/SKILL.md +1 -0
- package/bundled/skills/tdd-assistant/SKILL.md +2 -0
- package/bundled/templates/.ai-context.yaml.template +194 -0
- package/bundled/templates/CLAUDE.md.template +145 -0
- package/bundled/templates/DESIGN.md +237 -0
- package/bundled/templates/SKILL-BRIEF-TEMPLATE.md +57 -0
- package/bundled/templates/SKILL-CANDIDATES.md +39 -0
- package/bundled/templates/gates/check-error-exit.mjs +309 -0
- package/bundled/templates/mcp-config.json +10 -0
- package/bundled/templates/methodology-template.yaml +209 -0
- package/bundled/templates/migration-template.md +408 -0
- package/bundled/templates/requirement-checklist.md +410 -0
- package/bundled/templates/requirement-document-template.md +591 -0
- package/bundled/templates/requirement-template.md +881 -0
- package/bundled/templates/reverse-spec-template.md +409 -0
- package/bundled/templates/test-case-template.md +74 -0
- package/bundled/templates/test-plan-template.md +74 -0
- package/package.json +12 -9
- package/src/commands/audit.js +82 -0
- package/src/commands/check.js +227 -34
- package/src/commands/config.js +19 -19
- package/src/commands/init.js +174 -20
- package/src/commands/spec.js +2 -2
- package/src/commands/update.js +537 -43
- package/src/compilers/claude-code-compiler.js +4 -1
- package/src/config/ai-agent-paths.js +62 -17
- package/src/core/constants.js +42 -11
- package/src/core/manifest.js +201 -3
- package/src/core/paths.js +2 -2
- package/src/i18n/messages.js +42 -29
- package/src/installers/hooks-installer.js +167 -75
- package/src/installers/integration-installer.js +13 -9
- package/src/installers/skills-installer.js +4 -4
- package/src/installers/standards-installer.js +3 -3
- package/src/prompts/init.js +47 -18
- package/src/reconciler/diff-engine.js +57 -2
- package/src/reconciler/plan-executor.js +31 -11
- package/src/utils/detector.js +21 -1
- package/src/utils/effect-boundary.js +1093 -0
- package/src/utils/hasher.js +166 -1
- package/src/utils/hook-stats.js +1 -1
- package/src/utils/integration-generator.js +288 -82
- package/src/utils/reference-sync.js +107 -8
- package/src/utils/registry.js +57 -0
- package/src/utils/spinner.js +31 -0
- package/src/utils/yaml-generator.js +51 -9
- package/standards-registry.json +32 -9
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UDS Hook: Turn Completion Integrity
|
|
4
|
+
*
|
|
5
|
+
* Runs at turn end. Blocks when the agent's final message states a first-person
|
|
6
|
+
* commitment to a next action that the turn then ended without taking.
|
|
7
|
+
*
|
|
8
|
+
* Contract (Claude Code Stop hook):
|
|
9
|
+
* stdin — JSON with session_id, transcript_path, stop_hook_active
|
|
10
|
+
* block — print {"decision":"block","reason":"..."} on stdout, exit 0
|
|
11
|
+
* allow — print nothing, exit 0
|
|
12
|
+
*
|
|
13
|
+
* Every failure path allows. See core/turn-completion-integrity.md R5: a hook
|
|
14
|
+
* that can trap a session is worse than none, because the only recovery a human
|
|
15
|
+
* has is to disable it, and they will disable it permanently.
|
|
16
|
+
*
|
|
17
|
+
* Usage: node check-turn-completion.mjs (reads stdin)
|
|
18
|
+
* node check-turn-completion.mjs --self-test
|
|
19
|
+
* node check-turn-completion.mjs --languages
|
|
20
|
+
*
|
|
21
|
+
* @see docs/specs/SPEC-HOOKS-001-core-standard-hooks.md
|
|
22
|
+
* @see core/turn-completion-integrity.md
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { join, dirname } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { detectCommitment, userAskedToStop } from './turn-completion/detect.mjs';
|
|
29
|
+
|
|
30
|
+
export const VERSION = '1.1.0';
|
|
31
|
+
|
|
32
|
+
const COOLDOWN_SEC = 120;
|
|
33
|
+
// A cap counted per session with no reset is an off switch on a delay: it
|
|
34
|
+
// disarms silently in exactly the long session where the rule matters most.
|
|
35
|
+
// A rolling window bounds runaway loops just as well and recovers by itself.
|
|
36
|
+
const MAX_BLOCKS_PER_WINDOW = 6;
|
|
37
|
+
const WINDOW_SEC = 3600;
|
|
38
|
+
|
|
39
|
+
const STATE_DIR = join(homedir(), '.uds', 'turn-completion');
|
|
40
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
|
|
42
|
+
/** Locales this hook ships. The self-test fails if any of them will not load. */
|
|
43
|
+
export const SHIPPED_LOCALES = ['en', 'zh-TW'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load every shipped locale pack.
|
|
47
|
+
*
|
|
48
|
+
* At runtime a pack that fails to load is skipped (R5: a broken pack must not
|
|
49
|
+
* stop the turn from ending). 🔴 But that swallow is also how a shipping
|
|
50
|
+
* mistake hides: with zero packs loaded the hook runs, exits 0, and can never
|
|
51
|
+
* fire — the same shape as good behaviour. So `failed` is returned rather than
|
|
52
|
+
* discarded, and the self-test treats a non-empty `failed` as a failure.
|
|
53
|
+
* Caught while renaming the packs to .mjs: the loader still asked for .js.
|
|
54
|
+
*/
|
|
55
|
+
export async function loadPacks() {
|
|
56
|
+
const packs = [];
|
|
57
|
+
const failed = [];
|
|
58
|
+
for (const id of SHIPPED_LOCALES) {
|
|
59
|
+
try {
|
|
60
|
+
packs.push(await import(join(HERE, 'turn-completion', 'locales', `${id}.mjs`)));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
failed.push({ id, why: String((e && e.message) || e) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { packs, failed };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 🔴 This hook's own block message enters the transcript as a user turn. On the
|
|
70
|
+
* next run it would therefore be read as "the human's last message", hiding the
|
|
71
|
+
* real one ("pause, I'm going home") and silently voiding the R9 exemption.
|
|
72
|
+
*
|
|
73
|
+
* Same family as the detector matching the prose that documents it — except
|
|
74
|
+
* here it reads its own output. The reason string carries this line so it can
|
|
75
|
+
* recognise itself.
|
|
76
|
+
*/
|
|
77
|
+
export const SELF_ECHO = 'UDS standard turn-completion-integrity (R1)';
|
|
78
|
+
|
|
79
|
+
/** Plain text of a transcript message, or '' if it carries none. */
|
|
80
|
+
function textOf(msg) {
|
|
81
|
+
const c = msg && msg.content;
|
|
82
|
+
if (typeof c === 'string') return c;
|
|
83
|
+
if (!Array.isArray(c)) return '';
|
|
84
|
+
// Tool results also arrive with role "user"; only text blocks are prose.
|
|
85
|
+
const parts = c.filter((p) => p && p.type === 'text').map((p) => p.text || '');
|
|
86
|
+
return parts.some((p) => p.trim()) ? parts.join('\n') : '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Last assistant message and last human message in a Claude Code JSONL
|
|
91
|
+
* transcript. The human's message is needed because a turn that ends by their
|
|
92
|
+
* instruction looks, from the agent's words alone, exactly like one that ends
|
|
93
|
+
* on an abandoned commitment.
|
|
94
|
+
*/
|
|
95
|
+
export function lastMessages(transcriptPath) {
|
|
96
|
+
let assistant = '';
|
|
97
|
+
let user = '';
|
|
98
|
+
for (const line of readFileSync(transcriptPath, 'utf8').split('\n')) {
|
|
99
|
+
if (!line.trim()) continue;
|
|
100
|
+
let ev;
|
|
101
|
+
try { ev = JSON.parse(line); } catch { continue; }
|
|
102
|
+
const msg = ev && ev.message;
|
|
103
|
+
if (!msg) continue;
|
|
104
|
+
const t = textOf(msg);
|
|
105
|
+
if (!t) continue;
|
|
106
|
+
if (msg.role === 'assistant') assistant = t;
|
|
107
|
+
else if (msg.role === 'user' && !t.includes(SELF_ECHO)) user = t;
|
|
108
|
+
}
|
|
109
|
+
return { assistant, user };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readState(path) {
|
|
113
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function reason(packId, sentence) {
|
|
117
|
+
return [
|
|
118
|
+
'Your last message stated a next action, and then the turn ended without taking it.',
|
|
119
|
+
'',
|
|
120
|
+
`Detected by the ${packId} pack, in: "${sentence}"`,
|
|
121
|
+
'',
|
|
122
|
+
'UDS standard turn-completion-integrity (R1): a stated next action is not optional.',
|
|
123
|
+
'',
|
|
124
|
+
'Do one of these now:',
|
|
125
|
+
' (a) take the action you just said you would take;',
|
|
126
|
+
' (b) if it is actually blocked, say what blocks it and what you need, then do',
|
|
127
|
+
' the next item that is not blocked;',
|
|
128
|
+
' (c) if everything is done or blocked, list every remaining item WITH the',
|
|
129
|
+
' person or input it waits on. That itemized list is the only shape of',
|
|
130
|
+
' ending that R2 accepts — "the main parts are done" is not one.',
|
|
131
|
+
].join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
let raw = '';
|
|
136
|
+
try {
|
|
137
|
+
raw = readFileSync(0, 'utf8');
|
|
138
|
+
} catch {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let data;
|
|
143
|
+
try { data = JSON.parse(raw); } catch { return; }
|
|
144
|
+
if (!data || typeof data !== 'object') return;
|
|
145
|
+
if (data.stop_hook_active === true) return;
|
|
146
|
+
|
|
147
|
+
const tp = data.transcript_path;
|
|
148
|
+
if (!tp || !existsSync(tp)) return; // cannot tell is not the same as should block
|
|
149
|
+
|
|
150
|
+
const sid = String(data.session_id || 'unknown');
|
|
151
|
+
const statePath = join(STATE_DIR, `${sid}.json`);
|
|
152
|
+
const st = readState(statePath);
|
|
153
|
+
const now = Date.now() / 1000;
|
|
154
|
+
|
|
155
|
+
const stamps = (Array.isArray(st.stamps) ? st.stamps : [])
|
|
156
|
+
.filter((t) => typeof t === 'number' && now - t < WINDOW_SEC);
|
|
157
|
+
if (stamps.length >= MAX_BLOCKS_PER_WINDOW) return;
|
|
158
|
+
if (now - (st.last || 0) < COOLDOWN_SEC) return;
|
|
159
|
+
|
|
160
|
+
let msgs;
|
|
161
|
+
try { msgs = lastMessages(tp); } catch { return; }
|
|
162
|
+
if (!msgs.assistant.trim()) return;
|
|
163
|
+
|
|
164
|
+
const { packs } = await loadPacks();
|
|
165
|
+
if (packs.length === 0) return;
|
|
166
|
+
|
|
167
|
+
// The human asked for the turn to end. That is a legitimate ending, and the
|
|
168
|
+
// agent's own words cannot distinguish it from an abandoned commitment.
|
|
169
|
+
if (userAskedToStop(msgs.user, packs)) return;
|
|
170
|
+
|
|
171
|
+
const hit = detectCommitment(msgs.assistant, packs);
|
|
172
|
+
if (!hit.fired) return;
|
|
173
|
+
|
|
174
|
+
stamps.push(now);
|
|
175
|
+
try {
|
|
176
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
177
|
+
writeFileSync(statePath, JSON.stringify({ stamps, last: now }));
|
|
178
|
+
} catch {
|
|
179
|
+
/* state is an optimisation; failing to write it must not change the verdict */
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process.stdout.write(JSON.stringify({
|
|
183
|
+
decision: 'block',
|
|
184
|
+
reason: reason(hit.packId, hit.sentence),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function selfTest() {
|
|
189
|
+
const { packs, failed } = await loadPacks();
|
|
190
|
+
let ok = failed.length === 0 && packs.length === SHIPPED_LOCALES.length;
|
|
191
|
+
console.log(`[turn-completion] v${VERSION} — packs: ${packs.map((p) => p.id).join(', ')}`
|
|
192
|
+
+ ` (${packs.length}/${SHIPPED_LOCALES.length} shipped locales)`);
|
|
193
|
+
for (const f of failed) console.log(` x locale ${f.id} failed to load — ${f.why}`);
|
|
194
|
+
|
|
195
|
+
for (const pack of packs) {
|
|
196
|
+
for (const [want, label, text] of pack.corpus) {
|
|
197
|
+
// Run against ALL packs, which is the shipped configuration. A pack that
|
|
198
|
+
// is correct alone and wrong beside another is not correct.
|
|
199
|
+
const got = detectCommitment(text, packs).fired;
|
|
200
|
+
const good = got === want;
|
|
201
|
+
ok &&= good;
|
|
202
|
+
console.log(` ${good ? 'OK ' : 'x '} [${pack.id}] ${want ? 'must block' : 'must pass'} — ${label}`
|
|
203
|
+
+ (good ? '' : ` (got: ${got ? 'block' : 'pass'})`));
|
|
204
|
+
}
|
|
205
|
+
for (const [want, label, text] of pack.stopCorpus || []) {
|
|
206
|
+
const got = userAskedToStop(text, packs);
|
|
207
|
+
const good = got === want;
|
|
208
|
+
ok &&= good;
|
|
209
|
+
console.log(` ${good ? 'OK ' : 'x '} [${pack.id}] user ${want ? 'IS' : 'is NOT'} asking to stop — ${label}`
|
|
210
|
+
+ (good ? '' : ` (got: ${got ? 'exempt' : 'not exempt'})`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// The block message must contain the marker, or the hook cannot tell its own
|
|
214
|
+
// output from the human's next instruction.
|
|
215
|
+
const selfRecognised = reason('en', 'x').includes(SELF_ECHO);
|
|
216
|
+
ok &&= selfRecognised;
|
|
217
|
+
console.log(` ${selfRecognised ? 'OK ' : 'x '} block message carries the self-echo marker`);
|
|
218
|
+
|
|
219
|
+
console.log(`[turn-completion] self-test ${ok ? 'passed' : 'FAILED'}`);
|
|
220
|
+
process.exit(ok ? 0 : 1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const arg = process.argv[2];
|
|
224
|
+
if (arg === '--self-test') {
|
|
225
|
+
await selfTest();
|
|
226
|
+
} else if (arg === '--languages') {
|
|
227
|
+
const { packs } = await loadPacks();
|
|
228
|
+
console.log(packs.map((p) => `${p.id} (${p.label})`).join('\n'));
|
|
229
|
+
console.log('\nThis check reads prose. If you work in a language not listed above,'
|
|
230
|
+
+ '\nit is installed and running but cannot fire. See turn-completion-integrity R8.');
|
|
231
|
+
} else {
|
|
232
|
+
try { await main(); } catch { /* R5 */ }
|
|
233
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UDS Hook-Based Standard Injection
|
|
4
|
+
* Claude Code UserPromptSubmit hook that reads the user's prompt,
|
|
5
|
+
* matches it against manifest.json domain triggers, and outputs
|
|
6
|
+
* matching standard file paths as additional context.
|
|
7
|
+
*
|
|
8
|
+
* Usage: Configured as a Claude Code hook in .claude/settings.json
|
|
9
|
+
*
|
|
10
|
+
* Performance target: < 500ms
|
|
11
|
+
* Error handling: failures are non-blocking (exit 0 with empty output)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
15
|
+
import { join, dirname } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
function findManifest(cwd) {
|
|
21
|
+
const paths = [
|
|
22
|
+
join(cwd, '.standards', 'manifest.json'),
|
|
23
|
+
join(cwd, 'manifest.json'),
|
|
24
|
+
];
|
|
25
|
+
for (const p of paths) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
28
|
+
} catch {
|
|
29
|
+
// continue
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function matchDomains(prompt, manifest) {
|
|
36
|
+
const matched = new Set();
|
|
37
|
+
const promptLower = prompt.toLowerCase();
|
|
38
|
+
|
|
39
|
+
const allDomains = { ...(manifest.domains || {}) };
|
|
40
|
+
|
|
41
|
+
// Add custom domains from extensions
|
|
42
|
+
for (const ext of manifest.extensions || []) {
|
|
43
|
+
if (ext.type === 'custom-domain' && ext.domain && ext.triggers) {
|
|
44
|
+
allDomains[ext.domain] = {
|
|
45
|
+
_triggers: ext.triggers,
|
|
46
|
+
_standards: ext.standards || [],
|
|
47
|
+
_isCustom: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [domainName, domainValue] of Object.entries(allDomains)) {
|
|
53
|
+
if (domainName === 'always-on') continue;
|
|
54
|
+
|
|
55
|
+
// Get triggers: from ai.yaml structure or custom domain
|
|
56
|
+
let triggers = [];
|
|
57
|
+
if (domainValue._isCustom) {
|
|
58
|
+
triggers = domainValue._triggers;
|
|
59
|
+
} else {
|
|
60
|
+
// Built-in domains don't store triggers in manifest.json,
|
|
61
|
+
// use the domain name and known command patterns
|
|
62
|
+
const domainTriggerMap = {
|
|
63
|
+
testing: ['test', 'coverage', '/tdd', '/bdd', '/atdd', '/coverage', '.test.', '.spec.'],
|
|
64
|
+
specification: ['/sdd', '/spec', '/derive', '/reverse', '/requirement', 'docs/specs/'],
|
|
65
|
+
quality: ['/code-review', '/refactor', '/check', 'code review', 'pull request', 'security', 'performance'],
|
|
66
|
+
documentation: ['/docs', '/changelog', '/generate-docs', 'readme', 'changelog', 'documentation'],
|
|
67
|
+
workflow: ['/release', '/commit', 'branch', 'merge', 'release', 'deploy', 'version'],
|
|
68
|
+
architecture: ['architecture', 'project setup', 'error handling', 'logging', 'project structure'],
|
|
69
|
+
enforcement: ['hook', 'enforce', 'validate', 'dangerous', 'security check', 'logging check'],
|
|
70
|
+
};
|
|
71
|
+
triggers = domainTriggerMap[domainName] || [domainName];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const trigger of triggers) {
|
|
75
|
+
const triggerLower = trigger.toLowerCase();
|
|
76
|
+
if (triggerLower.startsWith('/')) {
|
|
77
|
+
// Slash command match
|
|
78
|
+
if (promptLower.includes(triggerLower)) {
|
|
79
|
+
if (domainValue._isCustom) {
|
|
80
|
+
domainValue._standards.forEach(s => matched.add(s));
|
|
81
|
+
} else if (Array.isArray(domainValue)) {
|
|
82
|
+
domainValue.forEach(s => matched.add(s));
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
} else if (triggerLower.includes('*')) {
|
|
87
|
+
// File pattern — check if prompt mentions matching files
|
|
88
|
+
const pattern = triggerLower.replace(/\*/g, '');
|
|
89
|
+
if (promptLower.includes(pattern)) {
|
|
90
|
+
if (domainValue._isCustom) {
|
|
91
|
+
domainValue._standards.forEach(s => matched.add(s));
|
|
92
|
+
} else if (Array.isArray(domainValue)) {
|
|
93
|
+
domainValue.forEach(s => matched.add(s));
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Keyword match
|
|
99
|
+
if (promptLower.includes(triggerLower)) {
|
|
100
|
+
if (domainValue._isCustom) {
|
|
101
|
+
domainValue._standards.forEach(s => matched.add(s));
|
|
102
|
+
} else if (Array.isArray(domainValue)) {
|
|
103
|
+
domainValue.forEach(s => matched.add(s));
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [...matched];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main() {
|
|
115
|
+
try {
|
|
116
|
+
// Read stdin
|
|
117
|
+
const chunks = [];
|
|
118
|
+
for await (const chunk of process.stdin) {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
}
|
|
121
|
+
const input = JSON.parse(Buffer.concat(chunks).toString());
|
|
122
|
+
|
|
123
|
+
const prompt = input.prompt || '';
|
|
124
|
+
const cwd = input.cwd || process.cwd();
|
|
125
|
+
|
|
126
|
+
if (!prompt) {
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const manifest = findManifest(cwd);
|
|
131
|
+
if (!manifest) {
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const matchedStandards = matchDomains(prompt, manifest);
|
|
136
|
+
|
|
137
|
+
// Record hook stats (silent, non-blocking)
|
|
138
|
+
try {
|
|
139
|
+
const configPath = join(cwd, '.uds', 'config.json');
|
|
140
|
+
let statsEnabled = false; // Default OFF — opt-in via .uds/config.json
|
|
141
|
+
try {
|
|
142
|
+
if (existsSync(configPath)) {
|
|
143
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
144
|
+
if (config.hookStats === true) statsEnabled = true;
|
|
145
|
+
}
|
|
146
|
+
} catch { /* default disabled */ }
|
|
147
|
+
|
|
148
|
+
if (statsEnabled) {
|
|
149
|
+
const statsDir = join(cwd, '.uds');
|
|
150
|
+
if (!existsSync(statsDir)) mkdirSync(statsDir, { recursive: true });
|
|
151
|
+
const statsPath = join(statsDir, 'hook-stats.jsonl');
|
|
152
|
+
const record = JSON.stringify({
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
matched_standards: matchedStandards.map(s => s.replace(/.*\//, '').replace('.ai.yaml', '')),
|
|
155
|
+
matched_count: matchedStandards.length,
|
|
156
|
+
total_available: Object.keys(manifest.domains || {}).length,
|
|
157
|
+
prompt_length: prompt.length
|
|
158
|
+
});
|
|
159
|
+
appendFileSync(statsPath, record + '\n');
|
|
160
|
+
}
|
|
161
|
+
} catch { /* silent failure — never block hook */ }
|
|
162
|
+
|
|
163
|
+
if (matchedStandards.length === 0) {
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Output as additional context
|
|
168
|
+
const context = [
|
|
169
|
+
'[UDS Context-Aware Loading] Matched standards for this prompt:',
|
|
170
|
+
...matchedStandards.map(s => ` - ${s}`),
|
|
171
|
+
'',
|
|
172
|
+
'Load these standards if not already in context.',
|
|
173
|
+
].join('\n');
|
|
174
|
+
|
|
175
|
+
process.stdout.write(context);
|
|
176
|
+
process.exit(0);
|
|
177
|
+
} catch {
|
|
178
|
+
// Non-blocking: exit 0 on any error
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UDS Hook Telemetry Wrapper
|
|
4
|
+
*
|
|
5
|
+
* Records hook execution telemetry to .standards/telemetry.jsonl.
|
|
6
|
+
* Format: {timestamp, standard_id, hook_type, result, duration_ms}
|
|
7
|
+
*
|
|
8
|
+
* If telemetryUpload=true and telemetryApiKey≠"" in .uds/config.json,
|
|
9
|
+
* also uploads the result to the remote telemetry server (opt-in).
|
|
10
|
+
*
|
|
11
|
+
* @see docs/specs/SPEC-TELEMETRY-001-hook-telemetry.md (REQ-1)
|
|
12
|
+
* @see docs/specs/SPEC-TELEMETRY-002-hook-upload.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync, statSync, writeFileSync } from 'fs';
|
|
16
|
+
import { join, dirname } from 'path';
|
|
17
|
+
|
|
18
|
+
export const TELEMETRY_FILE = '.standards/telemetry.jsonl';
|
|
19
|
+
const MAX_TELEMETRY_SIZE = 2 * 1024 * 1024; // 2MB
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Record a hook execution telemetry entry.
|
|
23
|
+
* @param {string} projectPath - Project root path
|
|
24
|
+
* @param {{ standard_id: string, hook_type: string, exitCode: number, duration_ms: number }} entry
|
|
25
|
+
*/
|
|
26
|
+
export function recordTelemetry(projectPath, entry) {
|
|
27
|
+
try {
|
|
28
|
+
const telPath = join(projectPath, TELEMETRY_FILE);
|
|
29
|
+
const dir = dirname(telPath);
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const record = {
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
standard_id: entry.standard_id,
|
|
37
|
+
hook_type: entry.hook_type,
|
|
38
|
+
result: entry.exitCode === 0 ? 'pass' : 'fail',
|
|
39
|
+
duration_ms: entry.duration_ms,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Rotation: truncate if exceeding 2MB
|
|
43
|
+
if (existsSync(telPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const size = statSync(telPath).size;
|
|
46
|
+
if (size > MAX_TELEMETRY_SIZE) {
|
|
47
|
+
const content = readFileSync(telPath, 'utf-8');
|
|
48
|
+
const lines = content.trim().split('\n');
|
|
49
|
+
const keepLines = lines.slice(Math.floor(lines.length / 2));
|
|
50
|
+
writeFileSync(telPath, keepLines.join('\n') + '\n');
|
|
51
|
+
}
|
|
52
|
+
} catch { /* ignore rotation errors */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
appendFileSync(telPath, JSON.stringify(record) + '\n');
|
|
56
|
+
} catch {
|
|
57
|
+
// Silently ignore write failures
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Upload hook result to remote telemetry server (opt-in via .uds/config.json).
|
|
63
|
+
* Silently fails — never blocks hook execution.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} projectPath - Project root path
|
|
66
|
+
* @param {{ standard_id: string, hook_type: string, exitCode: number, duration_ms: number }} entry
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*/
|
|
69
|
+
export async function uploadTelemetry(projectPath, entry) {
|
|
70
|
+
try {
|
|
71
|
+
// Dynamic import to avoid blocking if CLI module not available in hook context
|
|
72
|
+
const { uploadHookTelemetry } = await import('../../cli/src/utils/telemetry-uploader.js');
|
|
73
|
+
await uploadHookTelemetry(projectPath, entry);
|
|
74
|
+
} catch {
|
|
75
|
+
// Silently ignore upload failures (AC-4: never block hook)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared text preparation and pack runner for the turn-completion check.
|
|
3
|
+
*
|
|
4
|
+
* The language-specific part lives in ./locales/<id>.js. This file holds only
|
|
5
|
+
* what is true of every language, so that adding a language cannot accidentally
|
|
6
|
+
* change the behaviour of the ones already shipped.
|
|
7
|
+
*
|
|
8
|
+
* @see core/turn-completion-integrity.md (R7)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Remove the parts of a message that quote a pattern rather than enact it.
|
|
13
|
+
*
|
|
14
|
+
* Every exclusion here was added because its absence produced a false block:
|
|
15
|
+
* a detector reliably matches the text that documents it. Fenced code, inline
|
|
16
|
+
* backticks and markdown table rows are all places where an example of a
|
|
17
|
+
* commitment gets written down without anyone committing to anything.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} text
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function stripNonProse(text) {
|
|
23
|
+
return text
|
|
24
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter((line) => !line.trimStart().startsWith('|'))
|
|
27
|
+
.filter((line) => !line.trimStart().startsWith('>'))
|
|
28
|
+
.join('\n')
|
|
29
|
+
.replace(/`[^`\n]*`/g, ' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Split into paragraphs on blank lines. */
|
|
33
|
+
export function splitParagraphs(text) {
|
|
34
|
+
return text.split(/\n\s*\n/).filter((p) => p.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Split into sentences.
|
|
39
|
+
*
|
|
40
|
+
* Judgement is made per sentence, never per paragraph: a negated clause
|
|
41
|
+
* followed by a real commitment produces exactly one paragraph-level match,
|
|
42
|
+
* and the negation swallows the commitment.
|
|
43
|
+
*/
|
|
44
|
+
export function splitSentences(text) {
|
|
45
|
+
return text.split(/[.!?。!?\n]+/).filter((s) => s.trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Did the human ask for the turn to end?
|
|
50
|
+
*
|
|
51
|
+
* 🔴 The check reads only the agent's final message, so a turn that ends
|
|
52
|
+
* because the human said "pause, I'm going home" is indistinguishable from one
|
|
53
|
+
* that ends on an abandoned commitment — the agent's words are the same in both.
|
|
54
|
+
* Measured 2026-09-08: the first real firing after shipping was exactly this.
|
|
55
|
+
*
|
|
56
|
+
* A user-directed stop is the one legitimate ending the message-only design
|
|
57
|
+
* cannot represent, so the check has to look at the other side of the exchange.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} text - the human's most recent message
|
|
60
|
+
* @param {Array<{isStopRequest: (t: string) => boolean}>} packs
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function userAskedToStop(text, packs) {
|
|
64
|
+
if (!text || !text.trim()) return false;
|
|
65
|
+
return packs.some((p) => typeof p.isStopRequest === 'function' && p.isStopRequest(text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run every locale pack over one message.
|
|
70
|
+
*
|
|
71
|
+
* All packs run, and any one of them firing is enough. A bilingual transcript
|
|
72
|
+
* is the normal case, not the exception, and asking the adopter to configure
|
|
73
|
+
* which language they write in is a knob that will be set wrong.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} text - the assistant's final message
|
|
76
|
+
* @param {Array<{id: string, isCommitment: (s: string) => boolean, isAsking: (t: string) => boolean}>} packs
|
|
77
|
+
* @returns {{ fired: boolean, packId: string|null, sentence: string|null }}
|
|
78
|
+
*/
|
|
79
|
+
export function detectCommitment(text, packs) {
|
|
80
|
+
const body = stripNonProse(text);
|
|
81
|
+
|
|
82
|
+
for (const paragraph of splitParagraphs(body)) {
|
|
83
|
+
// A commitment that shares a paragraph with a request for information is
|
|
84
|
+
// conditional ("give me X and I will do Y"), not an unkept promise.
|
|
85
|
+
// Stopping there is correct behaviour, and blocking it teaches the adopter
|
|
86
|
+
// to uninstall the hook.
|
|
87
|
+
const asking = packs.some((p) => p.isAsking(paragraph));
|
|
88
|
+
if (asking) continue;
|
|
89
|
+
|
|
90
|
+
for (const sentence of splitSentences(paragraph)) {
|
|
91
|
+
for (const pack of packs) {
|
|
92
|
+
if (pack.isCommitment(sentence)) {
|
|
93
|
+
return { fired: true, packId: pack.id, sentence: sentence.trim().slice(0, 160) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { fired: false, packId: null, sentence: null };
|
|
99
|
+
}
|